From 0dcde6044392b8f6f22766a54d7999c80b4f8d96 Mon Sep 17 00:00:00 2001 From: zbeacon Date: Wed, 30 Nov 2022 13:06:00 +0200 Subject: [PATCH] Added alarm assignment feature and tests for it --- .../main/data/upgrade/3.4.2/schema_update.sql | 27 +++++++ .../server/controller/AlarmController.java | 73 +++++++++++++++-- .../controller/ControllerConstants.java | 2 + .../install/ThingsboardInstallService.java | 4 + .../service/action/EntityActionService.java | 6 ++ .../entitiy/AbstractTbEntityService.java | 2 +- .../DefaultTbNotificationEntityService.java | 4 + .../entitiy/alarm/DefaultTbAlarmService.java | 29 +++++++ .../service/entitiy/alarm/TbAlarmService.java | 5 ++ .../install/SqlDatabaseUpgradeService.java | 12 +++ .../update/DefaultDataUpdateService.java | 2 +- .../DefaultAlarmSubscriptionService.java | 19 ++++- .../controller/BaseAlarmControllerTest.java | 79 +++++++++++++++++++ .../server/dao/alarm/AlarmService.java | 7 +- .../server/common/data/DataConstants.java | 2 + .../server/common/data/alarm/Alarm.java | 25 +++--- .../server/common/data/alarm/AlarmQuery.java | 2 + .../server/common/data/alarm/EntityAlarm.java | 2 + .../server/common/data/audit/ActionType.java | 2 + .../common/data/edge/EdgeEventActionType.java | 2 + .../server/dao/alarm/AlarmDao.java | 3 +- .../server/dao/alarm/BaseAlarmService.java | 44 ++++++++++- .../server/dao/audit/AuditLogServiceImpl.java | 2 + .../server/dao/model/ModelConstants.java | 3 + .../dao/model/sql/AbstractAlarmEntity.java | 21 +++++ .../dao/model/sql/EntityAlarmEntity.java | 11 +++ .../server/dao/sql/alarm/AlarmRepository.java | 15 +++- .../server/dao/sql/alarm/JpaAlarmDao.java | 20 ++++- .../resources/sql/schema-entities-idx.sql | 2 + .../main/resources/sql/schema-entities.sql | 3 + .../dao/service/BaseAlarmServiceTest.java | 12 +-- .../engine/api/RuleEngineAlarmService.java | 7 +- .../components/alarm/alarm-table-config.ts | 2 +- .../home/models/widget-component.models.ts | 5 +- ui-ngx/src/app/shared/models/alarm.models.ts | 17 +++- .../src/app/shared/models/audit-log.models.ts | 4 + .../assets/locale/locale.constant-en_US.json | 3 + 37 files changed, 442 insertions(+), 38 deletions(-) create mode 100644 application/src/main/data/upgrade/3.4.2/schema_update.sql diff --git a/application/src/main/data/upgrade/3.4.2/schema_update.sql b/application/src/main/data/upgrade/3.4.2/schema_update.sql new file mode 100644 index 0000000000..e47a6bf2f3 --- /dev/null +++ b/application/src/main/data/upgrade/3.4.2/schema_update.sql @@ -0,0 +1,27 @@ +-- +-- Copyright © 2016-2022 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. +-- + + +-- ALARM ASSIGN TO USER START + +ALTER TABLE alarm ADD COLUMN assign_ts BIGINT; +ALTER TABLE alarm ADD COLUMN assignee_id UUID; + +ALTER TABLE entity_alarm ADD COLUMN assignee_id UUID; + +CREATE INDEX IF NOT EXISTS idx_entity_alarm_assignee_id ON entity_alarm(assignee_id); + +-- ALARM ASSIGN TO USER END \ No newline at end of file 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 858a7ee3c4..cb1093c745 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; 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.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -48,9 +49,13 @@ import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGNEE_ID; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGN_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; @@ -79,6 +84,7 @@ public class AlarmController extends BaseController { private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; + private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; @@ -179,6 +185,45 @@ public class AlarmController extends BaseController { tbAlarmService.clear(alarm, getCurrentUser()).get(); } + @ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)", + notes = "Assign the Alarm. " + + "Once assigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_ASSIGNED' " + + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = ASSIGN_ID_PARAM_DESCRIPTION) + @PathVariable(ASSIGNEE_ID) String strAssigneeId + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + checkParameter(ASSIGNEE_ID, strAssigneeId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + // TODO Add special permissions for assignment + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + UserId assigneeId = new UserId(UUID.fromString(strAssigneeId)); + tbAlarmService.assign(alarm, getCurrentUser(), assigneeId).get(); + } + + @ApiOperation(value = "Unassign Alarm (unassignAlarm)", + notes = "Unassign the Alarm. " + + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + // TODO Add special permissions for unassignment + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + tbAlarmService.unassign(alarm, getCurrentUser()).get(); + } + @ApiOperation(value = "Get Alarms (getAlarms)", notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @@ -194,6 +239,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -221,10 +268,14 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } catch (Exception e) { throw handleException(e); } @@ -244,6 +295,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -267,13 +320,17 @@ public class AlarmController extends BaseController { throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { if (getCurrentUser().isCustomerUser()) { - return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } else { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } } catch (Exception e) { throw handleException(e); @@ -295,7 +352,9 @@ public class AlarmController extends BaseController { @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) - @RequestParam(required = false) String status + @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -306,9 +365,13 @@ public class AlarmController extends BaseController { throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } checkEntityId(entityId, Operation.READ); try { - return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus); + return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus, assigneeUserId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index e00ccaa4bc..27d26164df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -27,6 +27,7 @@ public class ControllerConstants { protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; + protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "See the 'Model' tab of the Response Class for more details. "; @@ -44,6 +45,7 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ASSIGN_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'"; diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 9facea6dde..6b6104b0e2 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -236,6 +236,10 @@ public class ThingsboardInstallService { log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + case "3.4.2": + log.info("Upgrading ThingsBoard from version 3.4.2 to 3.5 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.4.2"); + break; //TODO update CacheCleanupService on the next version upgrade diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 7057f903be..fc8b71947c 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -86,6 +86,12 @@ public class EntityActionService { case ALARM_CLEAR: msgType = DataConstants.ALARM_CLEAR; break; + case ALARM_ASSIGN: + msgType = DataConstants.ALARM_CLEAR; + break; + case ALARM_UNASSIGN: + msgType = DataConstants.ALARM_CLEAR; + break; case ALARM_DELETE: msgType = DataConstants.ALARM_DELETE; break; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 9f34c061fe..f4885c56cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -73,7 +73,7 @@ public abstract class AbstractTbEntityService { protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); + alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 8f8531a5b4..94370d6ff2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -306,6 +306,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ALARM_ACK; case ALARM_CLEAR: return EdgeEventActionType.ALARM_CLEAR; + case ALARM_ASSIGN: + return EdgeEventActionType.ALARM_ASSIGN; + case ALARM_UNASSIGN: + return EdgeEventActionType.ALARM_UNASSIGN; case DELETED: return EdgeEventActionType.DELETED; case RELATION_ADD_OR_UPDATE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 8157eff3e5..023e144bf0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import java.util.List; @@ -75,6 +76,34 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb }, MoreExecutors.directExecutor()); } + @Override + public ListenableFuture assign(Alarm alarm, User user, UserId assigneeId) { + long assignTs = System.currentTimeMillis(); + ListenableFuture future = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, assignTs); + return Futures.transform(future, result -> { + if (result != null && result) { + alarm.setAssignTs(assignTs); + alarm.setAssigneeId(assigneeId); + notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ASSIGN, user); + } + return null; + }, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture unassign(Alarm alarm, User user) { + long assignTs = System.currentTimeMillis(); + ListenableFuture future = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs); + return Futures.transform(future, result -> { + if (result != null && result) { + alarm.setAssignTs(assignTs); + alarm.setAssigneeId(null); + notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_UNASSIGN, user); + } + return null; + }, MoreExecutors.directExecutor()); + } + @Override public Boolean delete(Alarm alarm, User user) { TenantId tenantId = alarm.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 8cd8d0b49d..0cf2de5ba9 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; public interface TbAlarmService { @@ -28,5 +29,9 @@ public interface TbAlarmService { ListenableFuture clear(Alarm alarm, User user); + ListenableFuture assign(Alarm alarm, User user, UserId assigneeId); + + ListenableFuture unassign(Alarm alarm, User user); + Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 80665d544f..349400ecd1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -677,6 +677,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.4.2": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Updating schema settings..."); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004003;"); + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index ee34bd7f23..a831a0af5e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -555,7 +555,7 @@ public class DefaultDataUpdateService implements DataUpdateService { }; private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { - AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, null, false); PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); boolean hasNext = true; while (hasNext) { 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 956dde8024..f7d121d9ff 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 @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -124,6 +125,20 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return result; } + @Override + public ListenableFuture assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { + ListenableFuture result = alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs); + Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + } + + @Override + public ListenableFuture unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { + ListenableFuture result = alarmService.unassignAlarm(tenantId, alarmId, assignTs); + Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + } + @Override public ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) { return alarmService.findAlarmByIdAsync(tenantId, alarmId); @@ -150,8 +165,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { - return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); + public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, UserId assigneeUserId) { + return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeUserId); } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 493b1818d9..05b6f15c61 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -347,6 +347,85 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorPermission))); } + @Test + public void testAssignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testReassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + + Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + logout(); + + loginCustomerUser(); + Mockito.reset(tbClusterService, auditLogService); + beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + customerUserId.getId()).andExpect(status().isOk()); + + foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(customerUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testUnassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + beforeAssignmentTs = System.currentTimeMillis(); + + doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); + foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); + } + @Test public void testFindAlarmsViaCustomerUser() throws Exception { loginCustomerUser(); 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 b8041aa3fc..81b4de263a 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 @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; 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.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -48,6 +49,10 @@ public interface AlarmService { ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + ListenableFuture assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); + + ListenableFuture unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); @@ -59,7 +64,7 @@ public interface AlarmService { ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus); + AlarmStatus alarmStatus, UserId assigneeUserId); ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 69c36697d3..099a452712 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -73,6 +73,8 @@ public class DataConstants { public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_CLEAR = "ALARM_CLEAR"; + public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; + public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index f13c84bc82..8eaca6b567 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.validation.Length; import java.util.List; @@ -58,23 +59,27 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha private AlarmSeverity severity; @ApiModelProperty(position = 9, required = true, value = "Alarm status", example = "CLEARED_UNACK") private AlarmStatus status; - @ApiModelProperty(position = 10, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + @ApiModelProperty(position = 10, value = "Alarm assignee user id") + private UserId assigneeId; + @ApiModelProperty(position = 11, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; - @ApiModelProperty(position = 11, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + @ApiModelProperty(position = 12, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; - @ApiModelProperty(position = 12, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") + @ApiModelProperty(position = 13, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") private long ackTs; - @ApiModelProperty(position = 13, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") + @ApiModelProperty(position = 14, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") private long clearTs; - @ApiModelProperty(position = 14, value = "JSON object with alarm details") + @ApiModelProperty(position = 15, value = "Timestamp of the alarm assigning0, in milliseconds", example = "1634115928465") + private long assignTs; + @ApiModelProperty(position = 16, value = "JSON object with alarm details") private transient JsonNode details; - @ApiModelProperty(position = 15, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") + @ApiModelProperty(position = 17, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; - @ApiModelProperty(position = 16, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") + @ApiModelProperty(position = 18, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") private boolean propagateToOwner; - @ApiModelProperty(position = 17, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") + @ApiModelProperty(position = 19, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") private boolean propagateToTenant; - @ApiModelProperty(position = 18, value = "JSON array of relation types that should be used for propagation. " + + @ApiModelProperty(position = 20, value = "JSON array of relation types that should be used for propagation. " + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") private List propagateRelationTypes; @@ -96,10 +101,12 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha this.originator = alarm.getOriginator(); this.severity = alarm.getSeverity(); this.status = alarm.getStatus(); + this.assigneeId = alarm.getAssigneeId(); this.startTs = alarm.getStartTs(); this.endTs = alarm.getEndTs(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 25bfefd4b2..945442dc9b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; /** @@ -33,6 +34,7 @@ public class AlarmQuery { private TimePageLink pageLink; private AlarmSearchStatus searchStatus; private AlarmStatus status; + private UserId assigneeId; private Boolean fetchOriginator; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java index e7606d852b..b75adf7ebb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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; @Data @NoArgsConstructor @@ -35,6 +36,7 @@ public class EntityAlarm implements HasTenantId { private String alarmType; private CustomerId customerId; + private UserId assigneeId; private AlarmId alarmId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 41f6f30dbe..4b666fa425 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -40,6 +40,8 @@ public enum ActionType { ALARM_ACK(false), ALARM_CLEAR(false), ALARM_DELETE(false), + ALARM_ASSIGN(false), + ALARM_UNASSIGN(false), LOGIN(false), LOGOUT(false), LOCKOUT(false), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index bba8767fca..79a689f2e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -31,6 +31,8 @@ public enum EdgeEventActionType { RPC_CALL, ALARM_ACK, ALARM_CLEAR, + ALARM_ASSIGN, + ALARM_UNASSIGN, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, CREDENTIALS_REQUEST, 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 87788d1b2b..11c3e47599 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 @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -58,7 +59,7 @@ public interface AlarmDao extends Dao { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); - Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status); + Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status, UserId assigneeUserId); PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); 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 e6012602f1..3abefb780b 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 @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -261,6 +262,42 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ }); } + @Override + public ListenableFuture assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { + return getAndUpdateAsync(tenantId, alarmId, new Function() { + @Nullable + @Override + public AlarmOperationResult apply(@Nullable Alarm alarm) { + if (alarm == null || assigneeId.equals(alarm.getAssigneeId())) { + return new AlarmOperationResult(alarm, false); + } else { + alarm.setAssigneeId(assigneeId); + alarm.setAssignTs(assignTime); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); + } + } + }); + } + + @Override + public ListenableFuture unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTime) { + return getAndUpdateAsync(tenantId, alarmId, new Function() { + @Nullable + @Override + public AlarmOperationResult apply(@Nullable Alarm alarm) { + if (alarm == null || alarm.getAssigneeId() == null) { + return new AlarmOperationResult(alarm, false); + } else { + alarm.setAssigneeId(null); + alarm.setAssignTs(assignTime); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); + } + } + }); + } + @Override public Alarm findAlarmById(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmById [{}]", alarmId); @@ -328,7 +365,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus) { + AlarmStatus alarmStatus, UserId assigneeUserId) { Set statusList = null; if (alarmSearchStatus != null) { statusList = alarmSearchStatus.getStatuses(); @@ -336,7 +373,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ statusList = Collections.singleton(alarmStatus); } - Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList); + Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList, assigneeUserId); return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } @@ -390,7 +427,8 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } private void createEntityAlarmRecord(TenantId tenantId, EntityId entityId, Alarm alarm) { - EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), alarm.getId()); + // TODO Add ability to automatically assign created alarm to some user + EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), null,alarm.getId()); try { alarmDao.createEntityAlarmRecord(entityAlarm); } catch (Exception e) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 8bb862bea5..8a08abcf3b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -165,6 +165,8 @@ public class AuditLogServiceImpl implements AuditLogService { case UPDATED: case ALARM_ACK: case ALARM_CLEAR: + case ALARM_ASSIGN: + case ALARM_UNASSIGN: case RELATIONS_DELETED: case ASSIGNED_TO_TENANT: if (entity != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e24bde91fb..c64dea0c40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -41,6 +41,7 @@ public class ModelConstants { public static final String USER_ID_PROPERTY = "user_id"; public static final String TENANT_ID_PROPERTY = "tenant_id"; public static final String CUSTOMER_ID_PROPERTY = "customer_id"; + public static final String ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String DEVICE_ID_PROPERTY = "device_id"; public static final String TITLE_PROPERTY = "title"; public static final String ALIAS_PROPERTY = "alias"; @@ -286,10 +287,12 @@ public class ModelConstants { public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_STATUS_PROPERTY = "status"; + public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String ALARM_START_TS_PROPERTY = "start_ts"; public static final String ALARM_END_TS_PROPERTY = "end_ts"; public static final String ALARM_ACK_TS_PROPERTY = "ack_ts"; public static final String ALARM_CLEAR_TS_PROPERTY = "clear_ts"; + public static final String ALARM_ASSIGN_TS_PROPERTY = "assign_ts"; public static final String ALARM_PROPAGATE_PROPERTY = "propagate"; public static final String ALARM_PROPAGATE_TO_OWNER_PROPERTY = "propagate_to_owner"; public static final String ALARM_PROPAGATE_TO_TENANT_PROPERTY = "propagate_to_tenant"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index 56adfae6de..c01b145174 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.springframework.data.annotation.Id; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -30,6 +31,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -44,6 +46,8 @@ import java.util.Collections; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGN_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; @@ -88,6 +92,10 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_STATUS_PROPERTY) private AlarmStatus status; + @Type(type="pg-uuid") + @Column(name = ALARM_ASSIGNEE_ID_PROPERTY) + private UUID assigneeId; + @Column(name = ALARM_START_TS_PROPERTY) private Long startTs; @@ -100,6 +108,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_CLEAR_TS_PROPERTY) private Long clearTs; + @Column(name = ALARM_ASSIGN_TS_PROPERTY) + private Long assignTs; + @Type(type = "json") @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) private JsonNode details; @@ -137,6 +148,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.type = alarm.getType(); this.severity = alarm.getSeverity(); this.status = alarm.getStatus(); + if (alarm.getAssigneeId() != null) { + this.assigneeId = alarm.getAssigneeId().getId(); + } this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); this.propagateToTenant = alarm.isPropagateToTenant(); @@ -144,6 +158,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarm.getEndTs(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); @@ -163,6 +178,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.type = alarmEntity.getType(); this.severity = alarmEntity.getSeverity(); this.status = alarmEntity.getStatus(); + this.assigneeId = alarmEntity.getAssigneeId(); this.propagate = alarmEntity.getPropagate(); this.propagateToOwner = alarmEntity.getPropagateToOwner(); this.propagateToTenant = alarmEntity.getPropagateToTenant(); @@ -170,6 +186,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarmEntity.getEndTs(); this.ackTs = alarmEntity.getAckTs(); this.clearTs = alarmEntity.getClearTs(); + this.assignTs = alarmEntity.getAssignTs(); this.details = alarmEntity.getDetails(); this.propagateRelationTypes = alarmEntity.getPropagateRelationTypes(); } @@ -187,6 +204,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setType(type); alarm.setSeverity(severity); alarm.setStatus(status); + if (assigneeId != null) { + alarm.setAssigneeId(new UserId(assigneeId)); + } alarm.setPropagate(propagate); alarm.setPropagateToOwner(propagateToOwner); alarm.setPropagateToTenant(propagateToTenant); @@ -194,6 +214,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setEndTs(endTs); alarm.setAckTs(ackTs); alarm.setClearTs(clearTs); + alarm.setAssignTs(assignTs); alarm.setDetails(details); if (!StringUtils.isEmpty(propagateRelationTypes)) { alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java index 1f1a40b25a..434a4662fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.ToData; import javax.persistence.Column; @@ -30,6 +31,7 @@ import javax.persistence.IdClass; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.ASSIGNEE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CREATED_TIME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ALARM_COLUMN_FAMILY_NAME; @@ -66,6 +68,9 @@ public final class EntityAlarmEntity implements ToData { @Column(name = CUSTOMER_ID_PROPERTY, columnDefinition = "uuid") private UUID customerId; + @Column(name = ASSIGNEE_ID_PROPERTY, columnDefinition = "uuid") + private UUID assigneeId; + public EntityAlarmEntity() { super(); } @@ -80,6 +85,9 @@ public final class EntityAlarmEntity implements ToData { if (entityAlarm.getCustomerId() != null) { customerId = entityAlarm.getCustomerId().getId(); } + if (entityAlarm.getAssigneeId() != null) { + assigneeId = entityAlarm.getAssigneeId().getId(); + } } @Override @@ -93,6 +101,9 @@ public final class EntityAlarmEntity implements ToData { if (customerId != null) { result.setCustomerId(new CustomerId(customerId)); } + if (assigneeId != null) { + result.setAssigneeId(new UserId(assigneeId)); + } return result; } 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 beadcddf31..24bfe88656 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 @@ -48,6 +48,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " @@ -63,6 +64,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -72,6 +74,7 @@ public interface AlarmRepository extends JpaRepository { @Param("startTime") Long startTime, @Param("endTime") Long endTime, @Param("alarmStatuses") Set alarmStatuses, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -80,6 +83,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ", @@ -90,6 +94,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -97,6 +102,7 @@ public interface AlarmRepository extends JpaRepository { @Param("startTime") Long startTime, @Param("endTime") Long endTime, @Param("alarmStatuses") Set alarmStatuses, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -105,6 +111,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " @@ -116,6 +123,7 @@ public interface AlarmRepository extends JpaRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))" + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -124,6 +132,7 @@ public interface AlarmRepository extends JpaRepository { @Param("startTime") Long startTime, @Param("endTime") Long endTime, @Param("alarmStatuses") Set alarmStatuses, + @Param("assigneeId") UUID assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -133,11 +142,13 @@ public interface AlarmRepository extends JpaRepository { "AND ea.tenantId = :tenantId " + "AND ea.entityId = :affectedEntityId " + "AND ea.entityType = :affectedEntityType " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (cast(:assigneeId as org.hibernate.type.UUIDCharType) IS NULL OR a.assigneeId = (:assigneeId))") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, - @Param("alarmStatuses") Set alarmStatuses); + @Param("alarmStatuses") Set alarmStatuses, + @Param("assigneeId") UUID assigneeId); @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.createdTime < :time AND a.endTs < :time") Page findAlarmsIdsByEndTsBeforeAndTenantId(@Param("time") Long time, @Param("tenantId") UUID tenantId, Pageable pageable); 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 407f7658fe..b862f1c7e7 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 @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -112,6 +113,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } else if (query.getStatus() != null) { statusSet = Collections.singleton(query.getStatus()); } + UUID assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().getId(); + } if (affectedEntity != null) { return DaoUtil.toPageData( alarmRepository.findAlarms( @@ -121,6 +126,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), statusSet, + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -132,6 +138,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), statusSet, + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -148,6 +155,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } else if (query.getStatus() != null) { statusSet = Collections.singleton(query.getStatus()); } + UUID assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().getId(); + } return DaoUtil.toPageData( alarmRepository.findCustomerAlarms( tenantId.getId(), @@ -155,6 +166,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), statusSet, + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -167,8 +179,12 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set statuses) { - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); + public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set statuses, UserId assigneeUserId) { + UUID assigneeId = null; + if (assigneeUserId != null) { + assigneeId = assigneeUserId.getId(); + } + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses, assigneeId); } @Override diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index e7586b17a8..2e1a830eb0 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -28,6 +28,8 @@ CREATE INDEX IF NOT EXISTS idx_entity_alarm_created_time ON entity_alarm(tenant_ CREATE INDEX IF NOT EXISTS idx_entity_alarm_alarm_id ON entity_alarm(alarm_id); +CREATE INDEX IF NOT EXISTS idx_entity_alarm_assignee_id ON entity_alarm(assignee_id); + CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 73039274a6..5ceb03288f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -52,7 +52,9 @@ CREATE TABLE IF NOT EXISTS alarm ( propagate boolean, severity varchar(255), start_ts bigint, + assign_ts bigint, status varchar(255), + assignee_id uuid, tenant_id uuid, customer_id uuid, propagate_relation_types varchar, @@ -69,6 +71,7 @@ CREATE TABLE IF NOT EXISTS entity_alarm ( alarm_type varchar(255) NOT NULL, customer_id uuid, alarm_id uuid, + assignee_id uuid, CONSTRAINT entity_alarm_pkey PRIMARY KEY (entity_id, alarm_id), CONSTRAINT fk_entity_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index f56936f87e..ba22a57110 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -425,7 +425,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { customerDevice = deviceService.saveDevice(customerDevice); // no one alarms was created - Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); Alarm alarm1 = Alarm.builder() .tenantId(tenantId) @@ -459,11 +459,11 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { .build(); alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm(); - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null)); - Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK)); + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null, null)); + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK, null)); } @Test 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 21bab255f2..e41c7ad5d5 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 @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.AlarmId; 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.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -49,6 +50,10 @@ public interface RuleEngineAlarmService { ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + ListenableFuture assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); + + ListenableFuture unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); @@ -61,7 +66,7 @@ public interface RuleEngineAlarmService { ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); - AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, UserId assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 6cc51857e7..ba70a43c14 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -116,7 +116,7 @@ export class AlarmTableConfig extends EntityTableConfig } fetchAlarms(pageLink: TimePageLink): Observable> { - const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true); + const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, null, true); return this.alarmService.getAlarms(query); } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 813b3b6927..7a7a81e302 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -89,6 +89,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; import { TelemetrySubscriber } from '@app/shared/public-api'; +import { UserId } from '@shared/models/id/user-id'; export interface IWidgetAction { name: string; @@ -414,8 +415,8 @@ export class WidgetContext { return new TimePageLink(pageSize, page, textSearch, sortOrder, startTime, endTime); } - alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean) { - return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator); + alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, assigneeId: UserId, fetchOriginator: boolean) { + return new AlarmQuery(entityId, pageLink, searchStatus, status, assigneeId, fetchOriginator); } } diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index fd2e200090..8059a1f585 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -23,6 +23,7 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { CustomerId } from '@shared/models/id/customer-id'; import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; +import { UserId } from "@shared/models/id/user-id"; export enum AlarmSeverity { CRITICAL = 'CRITICAL', @@ -89,6 +90,7 @@ export const alarmSeverityColors = new Map( export interface Alarm extends BaseData { tenantId: TenantId; customerId: CustomerId; + assigneeId: UserId; type: string; originator: EntityId; severity: AlarmSeverity; @@ -97,6 +99,7 @@ export interface Alarm extends BaseData { endTs: number; ackTs: number; clearTs: number; + assignTs: number; propagate: boolean; details?: any; } @@ -115,11 +118,13 @@ export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), customerId: new CustomerId(NULL_UUID), + assigneeId: new UserId(NULL_UUID), createdTime: new Date().getTime(), startTs: new Date().getTime(), endTs: 0, ackTs: 0, clearTs: 0, + assignTs: 0, originatorName: 'Simulated', originator: { entityType: EntityType.DEVICE, @@ -172,6 +177,12 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { name: 'alarm.clear-time', time: true }, + assignTime: { + keyName: 'assignTime', + value: 'assignTs', + name: 'alarm.assign-time', + time: true + }, originator: { keyName: 'originator', value: 'originatorName', @@ -205,15 +216,17 @@ export class AlarmQuery { pageLink: TimePageLink; searchStatus: AlarmSearchStatus; status: AlarmStatus; + assigneeId: UserId; fetchOriginator: boolean; constructor(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, - fetchOriginator: boolean) { + assigneeId: UserId, fetchOriginator: boolean) { this.affectedEntityId = entityId; this.pageLink = pageLink; this.searchStatus = searchStatus; this.status = status; + this.assigneeId = assigneeId; this.fetchOriginator = fetchOriginator; } @@ -224,6 +237,8 @@ export class AlarmQuery { query += `&searchStatus=${this.searchStatus}`; } else if (this.status) { query += `&status=${this.status}`; + } else if (this.assigneeId) { + query += `&assigneeId=${this.assigneeId.id}`; } if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { query += `&fetchOriginator=${this.fetchOriginator}`; diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 056c6aa569..fd9ca82275 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -47,6 +47,8 @@ export enum ActionType { RELATIONS_DELETED = 'RELATIONS_DELETED', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ALARM_ASSIGN = 'ALARM_ASSIGN', + ALARM_UNASSIGN = 'ALARM_UNASSIGN', LOGIN = 'LOGIN', LOGOUT = 'LOGOUT', LOCKOUT = 'LOCKOUT', @@ -85,6 +87,8 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_ASSIGN, 'audit-log.type-alarm-assign'], + [ActionType.ALARM_UNASSIGN, 'audit-log.type-alarm-unassign'], [ActionType.LOGIN, 'audit-log.type-login'], [ActionType.LOGOUT, 'audit-log.type-logout'], [ActionType.LOCKOUT, 'audit-log.type-lockout'], 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 3df8a15e52..269e2e0f67 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -440,6 +440,7 @@ "end-time": "End time", "ack-time": "Acknowledged time", "clear-time": "Cleared time", + "assign-time": "Assign time", "alarm-severity-list": "Alarm severity list", "any-severity": "Any severity", "severity-critical": "Critical", @@ -727,6 +728,8 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-alarm-assign": "Assigned", + "type-alarm-unassign": "Unassigned", "type-login": "Login", "type-logout": "Logout", "type-lockout": "Lockout",