From 14c61f65e9ec0bfdc78e4663cad74e769e2b80b7 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 21 Apr 2023 18:00:56 +0300 Subject: [PATCH] UI: Alarm count datasource type support. Improve alarm filters. --- .../server/controller/AlarmController.java | 137 +++++++ .../DefaultAlarmSubscriptionService.java | 11 + .../server/dao/alarm/AlarmService.java | 5 + .../common/data/alarm/AlarmQueryV2.java | 39 ++ .../server/dao/alarm/AlarmDao.java | 5 + .../server/dao/alarm/BaseAlarmService.java | 11 + .../server/dao/sql/alarm/AlarmRepository.java | 133 +++++++ .../server/dao/sql/alarm/JpaAlarmDao.java | 80 ++++ .../dao/service/BaseAlarmServiceTest.java | 110 ++++++ .../engine/api/RuleEngineAlarmService.java | 5 + ui-ngx/src/app/core/api/alias-controller.ts | 32 +- .../app/core/api/entity-data-subscription.ts | 91 ++++- .../src/app/core/api/entity-data.service.ts | 4 +- ui-ngx/src/app/core/http/alarm.service.ts | 14 +- ui-ngx/src/app/core/http/entity.service.ts | 26 +- .../core/services/dashboard-utils.service.ts | 69 +++- .../app/core/services/item-buffer.service.ts | 8 +- .../core/ws/telemetry-websocket.service.ts | 15 +- .../alarm/alarm-assignee-panel.component.html | 2 +- .../alarm/alarm-assignee-panel.component.ts | 2 + .../alarm-assignee-select-panel.component.ts | 203 ++++++++++ .../alarm-assignee-select.component.html | 31 ++ .../alarm/alarm-assignee-select.component.ts | 193 +++++++++ .../alarm/alarm-filter-config.component.html | 103 +++++ .../alarm-filter-config.component.scss} | 29 +- .../alarm/alarm-filter-config.component.ts | 318 +++++++++++++++ .../components/alarm/alarm-table-config.ts | 22 +- .../alarm/alarm-table-header.component.html | 14 +- .../alarm/alarm-table-header.component.scss | 6 +- .../alarm/alarm-table-header.component.ts | 10 +- .../components/alarm/alarm-table.component.ts | 61 ++- .../alias/entity-aliases-dialog.component.ts | 3 +- .../entity/entities-table.component.html | 2 +- .../entity/entities-table.component.scss | 3 + .../entity/entities-table.component.ts | 44 ++- .../home/components/home-components.module.ts | 9 + .../widget/data-keys.component.html | 8 +- .../components/widget/data-keys.component.ts | 12 +- .../lib/alarm-filter-panel.component.html | 65 ---- .../lib/alarm-filter-panel.component.ts | 119 ------ .../lib/alarms-table-widget.component.ts | 63 +-- .../widget/widget-components.module.ts | 40 +- .../widget/widget-config.component.html | 53 +-- .../widget/widget-config.component.scss | 7 + .../widget/widget-config.component.ts | 92 +---- .../entity/entities-table-config.models.ts | 1 + .../home-links/tenant_admin_home_page.raw | 55 ++- .../time/timewindow-panel.component.html | 5 + .../time/timewindow-panel.component.ts | 4 + .../components/time/timewindow.component.html | 10 +- .../components/time/timewindow.component.scss | 16 + .../components/time/timewindow.component.ts | 23 +- ui-ngx/src/app/shared/models/alarm.models.ts | 40 ++ .../app/shared/models/query/query.models.ts | 15 +- .../models/telemetry/telemetry.models.ts | 62 ++- .../src/app/shared/models/time/time.models.ts | 368 +++++++++--------- ui-ngx/src/app/shared/models/widget.models.ts | 21 +- .../assets/locale/locale.constant-en_US.json | 11 +- ui-ngx/src/styles.scss | 2 +- 59 files changed, 2255 insertions(+), 687 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html rename ui-ngx/src/app/modules/home/components/{widget/lib/alarm-filter-panel.component.scss => alarm/alarm-filter-config.component.scss} (66%) create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts 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 59ed65c6c9..cab8430fbf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -52,6 +53,10 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -85,9 +90,15 @@ public class AlarmController extends BaseController { private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; + + private static final String ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSearchStatus enumeration value"; 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_SEVERITY_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSeverity enumeration value"; + private static final String ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES = "CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE"; + + private static final String ALARM_QUERY_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing alarm types"; 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'."; @@ -331,6 +342,132 @@ public class AlarmController extends BaseController { } } + @ApiOperation(value = "Get Alarms (getAlarmsV2)", + notes = "Returns a page of alarms for the selected entity. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmsV2( + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] statusList, + @ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] severityList, + @ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION) + @RequestParam(required = false) String[] typeList, + @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) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION) + @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) + @RequestParam(required = false) Long endTime + ) throws ThingsboardException, ExecutionException, InterruptedException { + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + checkEntityId(entityId, Operation.READ); + List alarmStatusList = new ArrayList<>(); + if (statusList != null) { + for (String strStatus : statusList) { + if (!StringUtils.isEmpty(strStatus)) { + alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus)); + } + } + } + List alarmSeverityList = new ArrayList<>(); + if (severityList != null) { + for (String strSeverity : severityList) { + if (!StringUtils.isEmpty(strSeverity)) { + alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity)); + } + } + } + List alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList(); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + + return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); + } + + @ApiOperation(value = "Get All Alarms (getAllAlarmsV2)", + notes = "Returns a page of alarms that belongs to the current user owner. " + + "If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " + + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) + @ResponseBody + public PageData getAllAlarmsV2( + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] statusList, + @ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES) + @RequestParam(required = false) String[] severityList, + @ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION) + @RequestParam(required = false) String[] typeList, + @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) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder, + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION) + @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) + @RequestParam(required = false) Long endTime + ) throws ThingsboardException, ExecutionException, InterruptedException { + List alarmStatusList = new ArrayList<>(); + if (statusList != null) { + for (String strStatus : statusList) { + if (!StringUtils.isEmpty(strStatus)) { + alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus)); + } + } + } + List alarmSeverityList = new ArrayList<>(); + if (severityList != null) { + for (String strSeverity : severityList) { + if (!StringUtils.isEmpty(strSeverity)) { + alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity)); + } + } + } + List alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList(); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + + if (getCurrentUser().isCustomerUser()) { + return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); + } else { + return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get()); + } + } + @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH 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 07243711f3..e7f2a5a66f 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 @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findCustomerAlarms(tenantId, customerId, query); } + @Override + public ListenableFuture> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) { + return alarmService.findAlarmsV2(tenantId, query); + } + + @Override + public ListenableFuture> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) { + return alarmService.findCustomerAlarmsV2(tenantId, customerId, query); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); 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 75fa20fd57..fbf8892bd5 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -105,6 +106,10 @@ public interface AlarmService extends EntityDaoService { ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + ListenableFuture> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query); + + ListenableFuture> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java new file mode 100644 index 0000000000..94ff0fa8ac --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java @@ -0,0 +1,39 @@ +/** + * 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.common.data.alarm; + +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; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +public class AlarmQueryV2 { + + private EntityId affectedEntityId; + private TimePageLink pageLink; + private List typeList; + private List statusList; + private List severityList; + private UserId assigneeId; + +} 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 90134f0778..745861c6c8 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; @@ -66,6 +67,10 @@ public interface AlarmDao extends Dao { PageData findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + PageData findAlarmsV2(TenantId tenantId, AlarmQueryV2 query); + + PageData findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query); + PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); Set findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); 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 dbdce7718b..53a0631b77 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 @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmQuery; @@ -338,6 +339,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query)); } + @Override + public ListenableFuture> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) { + return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query)); + } + + @Override + public ListenableFuture> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) { + return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query)); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { 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 49e0257daf..2f7787c9cc 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 @@ -90,6 +90,57 @@ public interface AlarmRepository extends JpaRepository { @Param("searchText") String searchText, Pageable pageable); + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + + "WHERE a.tenantId = :tenantId " + + "AND ea.tenantId = :tenantId " + + "AND ea.entityId = :affectedEntityId " + + "AND ea.entityType = :affectedEntityType " + + "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 ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) " + , + countQuery = "" + + "SELECT count(a) " + //alarms with relations only + "FROM AlarmInfoEntity a " + + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + + "WHERE a.tenantId = :tenantId " + + "AND ea.tenantId = :tenantId " + + "AND ea.entityId = :affectedEntityId " + + "AND ea.entityType = :affectedEntityType " + + "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 ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) ") + Page findAlarmsV2(@Param("tenantId") UUID tenantId, + @Param("affectedEntityId") UUID affectedEntityId, + @Param("affectedEntityType") String affectedEntityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmTypes") List alarmTypes, + @Param("alarmSeverities") List alarmSeverities, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, + @Param("searchText") String searchText, + Pageable pageable); + @Query(value = "SELECT a " + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId " + @@ -124,6 +175,46 @@ public interface AlarmRepository extends JpaRepository { @Param("searchText") String searchText, Pageable pageable); + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + + "WHERE a.tenantId = :tenantId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) ", + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmInfoEntity a " + + "WHERE a.tenantId = :tenantId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) ") + Page findAllAlarmsV2(@Param("tenantId") UUID tenantId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmTypes") List alarmTypes, + @Param("alarmSeverities") List alarmSeverities, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, + @Param("searchText") String searchText, + Pageable pageable); + @Query(value = "SELECT a " + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + @@ -160,6 +251,48 @@ public interface AlarmRepository extends JpaRepository { @Param("searchText") String searchText, Pageable pageable); + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) " + , + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmInfoEntity a " + + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(: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, '%'))) ") + Page findCustomerAlarmsV2(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmTypes") List alarmTypes, + @Param("alarmSeverities") List alarmSeverities, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, + @Param("searchText") String searchText, + Pageable pageable); + @Query(value = "SELECT a.severity FROM AlarmEntity a " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 4376b4d3c2..98d607735e 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.alarm.AlarmCreateOrUpdateActiveRequest import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; @@ -200,6 +201,85 @@ public class JpaAlarmDao extends JpaAbstractDao implements A ); } + @Override + public PageData findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) { + log.trace("Try to find alarms by entity [{}], query [{}] and pageLink [{}]", query.getAffectedEntityId(), query, query.getPageLink()); + EntityId affectedEntity = query.getAffectedEntityId(); + List typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null; + List severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null; + AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList()); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); + } + if (affectedEntity != null) { + return DaoUtil.toPageData( + alarmRepository.findAlarmsV2( + tenantId.getId(), + affectedEntity.getId(), + affectedEntity.getEntityType().name(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + typeList, + severityList, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } else { + return DaoUtil.toPageData( + alarmRepository.findAllAlarmsV2( + tenantId.getId(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + typeList, + severityList, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } + } + + @Override + public PageData findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) { + log.trace("Try to find customer alarms by query [{}] and pageLink [{}]", query, query.getPageLink()); + List typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null; + List severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null; + AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList()); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); + } + return DaoUtil.toPageData( + alarmRepository.findCustomerAlarmsV2( + tenantId.getId(), + customerId.getId(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + typeList, + severityList, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } + @Override public PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); 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 f051ac2963..7f5c6a3d72 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -221,6 +222,115 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); } + @Test + public void testFindAlarmV2() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); + + long ts = System.currentTimeMillis(); + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); + + // Check child relation + PageData alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(childId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + + // Check parent relation + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(parentId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(0, alarms.getData().size()); + + created.setPropagate(true); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); + created = result.getAlarm(); + + // Check child relation + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(childId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + + // Check parent relation + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(parentId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + + alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); + + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(childId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + + // Check not existing relation + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(childId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(0, alarms.getData().size()); + + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); + + alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() + .affectedEntityId(childId) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL)) + .statusList(Arrays.asList(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink( + new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + } + @Test public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { 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 ed0579387d..a9387c6180 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 @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -102,6 +103,10 @@ public interface RuleEngineAlarmService { ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + ListenableFuture> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query); + + ListenableFuture> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 2f2a90b7e7..d6d90eadad 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -24,6 +24,8 @@ import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/mode import { EntityInfo } from '@shared/models/entity.models'; import { map, mergeMap } from 'rxjs/operators'; import { + AlarmFilter, + AlarmFilterConfig, createDefaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, updateDatasourceFromEntityInfo @@ -244,10 +246,14 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); - if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) { + if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount + || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); } + if (newDatasource.type === DatasourceType.alarmCount) { + newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig); + } if (newDatasource.entityAliasId) { return this.getAliasInfo(newDatasource.entityAliasId).pipe( mergeMap((aliasInfo) => { @@ -336,22 +342,30 @@ export class AliasController implements IAliasController { map((result) => { let functionIndex = 0; let entityCountIndex = 0; + let alarmCountIndex = 0; result.forEach((datasource) => { - if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) { + if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount || + datasource.type === DatasourceType.alarmCount) { let name: string; if (datasource.name && datasource.name.length) { name = datasource.name; } else { + name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); if (datasource.type === DatasourceType.function) { functionIndex++; - } else { + if (functionIndex > 1) { + name += ' ' + functionIndex; + } + } else if (datasource.type === DatasourceType.entityCount) { entityCountIndex++; - } - name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); - if (datasource.type === DatasourceType.function && functionIndex > 1) { - name += ' ' + functionIndex; - } else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { - name += ' ' + entityCountIndex; + if (entityCountIndex > 1) { + name += ' ' + entityCountIndex; + } + } else { + alarmCountIndex++; + if (alarmCountIndex > 1) { + name += ' ' + alarmCountIndex; + } } } datasource.name = name; diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index a49ce76470..25cb47ca0a 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -23,6 +23,7 @@ import { SubscriptionTimewindow } from '@shared/models/time/time.models'; import { + AlarmFilter, ComparisonTsValue, EntityData, EntityDataPageLink, @@ -36,6 +37,7 @@ import { } from '@shared/models/query/query.models'; import { AggKey, + AlarmCountCmd, DataKeyType, EntityCountCmd, EntityDataCmd, @@ -84,6 +86,7 @@ export interface EntityDataSubscriptionOptions { dataKeys: Array; type: widgetType; entityFilter?: EntityFilter; + alarmFilter?: AlarmFilter; isPaginatedDataSubscription?: boolean; ignoreDataUpdateOnIntervalTick?: boolean; pageLink?: EntityDataPageLink; @@ -111,6 +114,7 @@ export class EntityDataSubscription { private dataCommand: EntityDataCmd; private subsCommand: EntityDataCmd; private countCommand: EntityCountCmd; + private alarmCountCommand: AlarmCountCmd; private attrFields: Array; private tsFields: Array; @@ -197,6 +201,7 @@ export class EntityDataSubscription { } let key: string; if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.datasourceType === DatasourceType.alarmCount || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.datasourceType === DatasourceType.function) { key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; @@ -228,7 +233,8 @@ export class EntityDataSubscription { clearTimeout(this.latestTimer); this.latestTimer = null; } - if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount + || this.datasourceType === DatasourceType.alarmCount) { if (this.subscriber) { this.subscriber.unsubscribe(); this.subscriber = null; @@ -508,6 +514,84 @@ export class EntityDataSubscription { } ); this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.alarmCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); + this.alarmCountCommand = new AlarmCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + this.alarmCountCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + if (this.entityDataSubscriptionOptions.alarmFilter) { + this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; + } + this.subscriber.subscriptionCommands.push(this.alarmCountCommand); + + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; + + const countKey = this.dataKeysList[0]; + + let dataReceived = false; + + this.subscriber.alarmCount$.subscribe( + (alarmCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now() + this.latestTsOffset, + value: DatasourceType.alarmCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; + } else { + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); + } + } + ); + this.subscriber.subscribe(); } if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { return of(null); @@ -542,7 +626,7 @@ export class EntityDataSubscription { this.subscriber.subscriptionCommands = [this.subsCommand]; this.subscriber.update(); } - } else if (this.datasourceType === DatasourceType.entityCount) { + } else if (this.datasourceType === DatasourceType.entityCount || this.datasourceType === DatasourceType.alarmCount) { if (this.subscriber.setTsOffset(this.latestTsOffset)) { if (this.listener.forceReInit) { this.listener.forceReInit(); @@ -729,7 +813,8 @@ export class EntityDataSubscription { this.datasourceData[dataIndex] = {}; for (const key of Object.keys(this.dataKeys)) { const dataKey = this.dataKeys[key]; - if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount + || this.datasourceType === DatasourceType.alarmCount || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { const dataKeysList = dataKey as Array; for (let index = 0; index < dataKeysList.length; index++) { diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index d10ffa9a13..691f1b3d29 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -175,8 +175,10 @@ export class EntityDataService { type: subscriptionType }; if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || - entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { + entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount || + entityDataSubscriptionOptions.datasourceType === DatasourceType.alarmCount) { entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; + entityDataSubscriptionOptions.alarmFilter = datasource.alarmFilter; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index 91db205c2d..e03e9ab783 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -23,7 +23,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { Alarm, AlarmInfo, - AlarmQuery, + AlarmQuery, AlarmQueryV2, AlarmSearchStatus, AlarmSeverity, AlarmStatus @@ -78,12 +78,24 @@ export class AlarmService { defaultHttpOptionsFromConfig(config)); } + public getAlarmsV2(query: AlarmQueryV2, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/v2/alarm${query.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + public getAllAlarms(query: AlarmQuery, config?: RequestConfig): Observable> { return this.http.get>(`/api/alarms${query.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getAllAlarmsV2(query: AlarmQueryV2, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/v2/alarms${query.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, config?: RequestConfig): Observable { let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`; diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index f4a7217b34..a1e116b350 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -65,7 +65,7 @@ import { Device, DeviceCredentialsType } from '@shared/models/device.models'; import { AttributeService } from '@core/http/attribute.service'; import { AlarmData, - AlarmDataQuery, + AlarmDataQuery, AlarmFilter, AlarmFilterConfig, createDefaultEntityDataPageLink, EntityData, EntityDataQuery, @@ -91,6 +91,7 @@ import { AssetProfileService } from '@core/http/asset-profile.service'; import { NotificationService } from '@core/http/notification.service'; import { TenantProfileService } from '@core/http/tenant-profile.service'; import { NotificationType } from '@shared/models/notification.models'; +import { UserId } from '@shared/models/id/user-id'; @Injectable({ providedIn: 'root' @@ -1010,6 +1011,24 @@ export class EntityService { ); } + public resolveAlarmFilter(alarmFilterConfig?: AlarmFilterConfig): AlarmFilter { + const alarmFilter: AlarmFilter = {}; + if (alarmFilterConfig) { + alarmFilter.typeList = alarmFilterConfig.typeList; + alarmFilter.severityList = alarmFilterConfig.severityList; + alarmFilter.statusList = alarmFilterConfig.statusList; + alarmFilter.searchPropagatedAlarms = isDefined(alarmFilterConfig.searchPropagatedAlarms) ? + alarmFilterConfig.searchPropagatedAlarms : true; + if (alarmFilterConfig.assignedToCurrentUser) { + const authUser = getCurrentAuthUser(this.store); + alarmFilter.assigneeId = new UserId(authUser.userId); + } else { + alarmFilter.assigneeId = alarmFilterConfig.assigneeId; + } + } + return alarmFilter; + } + public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean, config?: RequestConfig): Observable { const saveEntityObservable: Observable> = this.getSaveEntityObservable(entityType, entityData, config); @@ -1321,7 +1340,8 @@ export class EntityService { dataKeys: [] }; this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); - } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) { + } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount || + subscriptionInfo.type === DatasourceType.alarmCount) { datasource = { type: subscriptionInfo.type, name: subscriptionInfo.name || subscriptionInfo.type, @@ -1341,7 +1361,7 @@ export class EntityService { if (subscriptionInfo.alarmFields) { this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); } - if (subscriptionInfo.type === DatasourceType.entityCount) { + if (subscriptionInfo.type === DatasourceType.entityCount || subscriptionInfo.type === DatasourceType.alarmCount) { const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count); datasource.dataKeys.push(dataKey); } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index a56b8f1708..6c36dde958 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -30,11 +30,12 @@ import { WidgetLayout } from '@shared/models/dashboard.models'; import { isDefined, isString, isUndefined } from '@core/utils'; -import { Datasource, DatasourceType, Widget, widgetType } from '@app/shared/models/widget.models'; +import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { EntityId } from '@app/shared/models/id/entity-id'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; +import { AlarmSearchStatus } from '@shared/models/alarm.models'; @Injectable({ providedIn: 'root' @@ -201,13 +202,22 @@ export class DashboardUtilsService { } public validateAndUpdateWidget(widget: Widget): Widget { - if (!widget.config) { - widget.config = {}; + widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); + // Temp workaround + if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { + widget.typeAlias = 'basic_timeseries'; + } + return widget; + } + + public validateAndUpdateWidgetConfig(widgetConfig: WidgetConfig | undefined, type: widgetType): WidgetConfig { + if (!widgetConfig) { + widgetConfig = {}; } - if (!widget.config.datasources) { - widget.config.datasources = []; + if (!widgetConfig.datasources) { + widgetConfig.datasources = []; } - widget.config.datasources.forEach((datasource) => { + widgetConfig.datasources.forEach((datasource) => { if (datasource.type === 'device') { datasource.type = DatasourceType.entity; } @@ -216,14 +226,47 @@ export class DashboardUtilsService { delete datasource.deviceAliasId; } }); - if (widget.type === widgetType.latest) { - widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService); - } - // Temp workaround - if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { - widget.typeAlias = 'basic_timeseries'; + if (type === widgetType.latest) { + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService); + } + if (type === widgetType.alarm) { + if (!widgetConfig.alarmFilterConfig) { + widgetConfig.alarmFilterConfig = {}; + const alarmFilterConfig = widgetConfig.alarmFilterConfig; + if (isDefined(widgetConfig.alarmStatusList) && widgetConfig.alarmStatusList.length) { + alarmFilterConfig.statusList = widgetConfig.alarmStatusList; + } else if (isDefined(widgetConfig.alarmSearchStatus) && widgetConfig.alarmSearchStatus !== AlarmSearchStatus.ANY) { + alarmFilterConfig.statusList = [widgetConfig.alarmSearchStatus]; + } else { + alarmFilterConfig.statusList = []; + } + if (isDefined(widgetConfig.alarmStatusList)) { + delete widgetConfig.alarmStatusList; + } + if (isDefined(widgetConfig.alarmSearchStatus)) { + delete widgetConfig.alarmSearchStatus; + } + if (isDefined(widgetConfig.alarmSeverityList)) { + alarmFilterConfig.severityList = widgetConfig.alarmSeverityList; + delete widgetConfig.alarmSeverityList; + } else { + alarmFilterConfig.severityList = []; + } + if (isDefined(widgetConfig.alarmTypeList)) { + alarmFilterConfig.typeList = widgetConfig.alarmTypeList; + delete widgetConfig.alarmTypeList; + } else { + alarmFilterConfig.typeList = []; + } + if (isDefined(widgetConfig.searchPropagatedAlarms)) { + alarmFilterConfig.searchPropagatedAlarms = widgetConfig.searchPropagatedAlarms; + delete widgetConfig.searchPropagatedAlarms; + } else { + alarmFilterConfig.searchPropagatedAlarms = true; + } + } } - return widget; + return widgetConfig; } public createDefaultLayoutData(): DashboardLayout { diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index a9d9143516..65dfc0e204 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -101,7 +101,9 @@ export class ItemBufferService { if (datasources) { for (let i = 0; i < datasources.length; i++) { const datasource = datasources[i]; - if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) { + if ((datasource.type === DatasourceType.entity || + datasource.type === DatasourceType.entityCount || + datasource.type === DatasourceType.alarmCount) && datasource.entityAliasId) { entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; if (entityAlias) { aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); @@ -127,7 +129,9 @@ export class ItemBufferService { if (datasources) { for (let i = 0; i < datasources.length; i++) { const datasource = datasources[i]; - if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) { + if ((datasource.type === DatasourceType.entity || + datasource.type === DatasourceType.entityCount || + datasource.type === DatasourceType.alarmCount) && datasource.filterId) { filter = dashboard.configuration.filters[datasource.filterId]; if (filter) { filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts index c98d0f429c..56c35328fb 100644 --- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -16,6 +16,8 @@ import { Inject, Injectable, NgZone } from '@angular/core'; import { + AlarmCountCmd, AlarmCountUnsubscribeCmd, + AlarmCountUpdate, AlarmDataCmd, AlarmDataUnsubscribeCmd, AlarmDataUpdate, @@ -26,7 +28,7 @@ import { EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, - GetHistoryCmd, + GetHistoryCmd, isAlarmCountUpdateMsg, isAlarmDataUpdateMsg, isEntityCountUpdateMsg, isEntityDataUpdateMsg, @@ -80,6 +82,8 @@ export class TelemetryWebsocketService extends WebsocketService account_circle - alarm.unassigned + {{ assigneeNotSetText | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts index ce448d0321..30fa807027 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -65,6 +65,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe assigneeId?: string; + assigneeNotSetText = 'alarm.unassigned'; + reassigned: boolean = false; selectUserFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts new file mode 100644 index 0000000000..1526616833 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts @@ -0,0 +1,203 @@ +/// +/// 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. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Inject, + InjectionToken, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, of, Subject } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, takeUntil, } from 'rxjs/operators'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { TranslateService } from '@ngx-translate/core'; +import { UserService } from '@core/http/user.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { UtilsService } from '@core/services/utils.service'; + +export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken('AlarmAssigneeSelectPanelData'); + +export interface AlarmAssigneeSelectPanelData { + assigneeId?: string; +} + +@Component({ + selector: 'tb-alarm-assignee-select-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'] +}) +export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { + + private dirty = false; + + assigneeId?: string; + + assigneeNotSetText = 'alarm.assignee-not-set'; + + selectUserFormGroup: FormGroup; + + @ViewChild('userInput', {static: true}) userInput: ElementRef; + + filteredUsers: Observable>; + + searchText = ''; + + userSelected = false; + + result?: UserEmailInfo; + + private destroy$ = new Subject(); + + constructor(@Inject(ALARM_ASSIGNEE_SELECT_PANEL_DATA) public data: AlarmAssigneeSelectPanelData, + public overlayRef: OverlayRef, + public translate: TranslateService, + private userService: UserService, + private fb: FormBuilder, + private utilsService: UtilsService) { + this.assigneeId = data.assigneeId; + this.selectUserFormGroup = this.fb.group({ + user: [null] + }); + } + + ngOnInit() { + this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges + .pipe( + debounceTime(150), + map(value => value ? (typeof value === 'string' ? value : '') : ''), + distinctUntilChanged(), + switchMap(name => this.fetchUsers(name)), + share(), + takeUntil(this.destroy$) + ); + } + + ngAfterViewInit() { + setTimeout(() => { + this.userInput.nativeElement.focus(); + }, 0); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + displayUserFn(user?: User): string | undefined { + return user ? user.email : undefined; + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.clear(); + this.userSelected = true; + this.result = event.option.value; + this.overlayRef.dispose(); + } + + fetchUsers(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'email', + direction: Direction.ASC + }); + return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) + .pipe( + catchError(() => of(emptyPageData())), + map(pageData => pageData.data) + ); + } + + onFocus(): void { + if (!this.dirty) { + this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); + this.dirty = true; + } + } + + clear() { + this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.userInput.nativeElement.blur(); + this.userInput.nativeElement.focus(); + }, 0); + } + + getUserDisplayName(entity: UserEmailInfo) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: UserEmailInfo): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: UserEmailInfo): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: UserEmailInfo) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html new file mode 100644 index 0000000000..2ad7229365 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html @@ -0,0 +1,31 @@ + + + + alarm.assignee + + + {{ getUserInitials() }} + + account_circle + arrow_drop_down + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts new file mode 100644 index 0000000000..4d1f6021be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts @@ -0,0 +1,193 @@ +/// +/// 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. +/// + +import { Component, forwardRef, Injector, Input, OnInit, StaticProvider, ViewContainerRef } from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { TranslateService } from '@ngx-translate/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UserId } from '@shared/models/id/user-id'; +import { UserService } from '@core/http/user.service'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { catchError, map, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { + ALARM_ASSIGNEE_SELECT_PANEL_DATA, + AlarmAssigneeSelectPanelComponent, + AlarmAssigneeSelectPanelData +} from '@home/components/alarm/alarm-assignee-select-panel.component'; + +@Component({ + selector: 'tb-alarm-assignee-select', + templateUrl: './alarm-assignee-select.component.html', + styleUrls: ['./alarm-assignee.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmAssigneeSelectComponent), + multi: true + } + ] +}) +export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor { + + @Input() disabled: boolean; + + assigneeFormGroup: UntypedFormGroup; + assignee?: User | UserEmailInfo; + + private propagateChange = (_: any) => {}; + + constructor(private utilsService: UtilsService, + private overlay: Overlay, + private fb: UntypedFormBuilder, + private userService: UserService, + private viewContainerRef: ViewContainerRef, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.assigneeFormGroup = this.fb.group({ + assignee: [null, []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.assigneeFormGroup.disable({emitEvent: false}); + } else { + this.assigneeFormGroup.enable({emitEvent: false}); + } + } + + writeValue(userId?: UserId): void { + const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe( + catchError(() => of(null)) + ) : of(null); + userObservable.pipe( + tap((user) => { + this.assignee = user; + }), + map((user) => this.getAssignee(user)) + ).subscribe((assignee) => { + this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); + }); + } + + private getAssignee(user?: User| UserEmailInfo): string { + if (user) { + return this.getUserDisplayName(user); + } else { + return this.translateService.instant('alarm.assignee-not-set'); + } + } + + private getUserDisplayName(user?: User | UserEmailInfo): string { + let displayName = ''; + if ((user?.firstName && user?.firstName.length > 0) || + (user?.lastName && user?.lastName.length > 0)) { + if (user?.firstName) { + displayName += user?.firstName; + } + if (user?.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += user?.lastName; + } + } else { + displayName = user?.email; + } + return displayName; + } + + getUserInitials(): string { + let initials = ''; + if (this.assignee?.firstName && this.assignee?.firstName.length || + this.assignee?.lastName && this.assignee?.lastName.length) { + if (this.assignee?.firstName) { + initials += this.assignee?.firstName.charAt(0); + } + if (this.assignee?.lastName) { + initials += this.assignee?.lastName.charAt(0); + } + } else { + initials += this.assignee?.email.charAt(0); + } + return initials.toUpperCase(); + } + + getAvatarBgColor(): string { + return this.utilsService.stringToHslColor(this.getUserDisplayName(this.assignee), 40, 60); + } + + openAlarmAssigneeSelectPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + if (!this.disabled) { + const target = $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.width = (target as HTMLElement).offsetWidth; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_SELECT_PANEL_DATA, + useValue: { + assigneeId: this.assignee?.id?.id + } as AlarmAssigneeSelectPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const component = overlayRef.attach(new ComponentPortal(AlarmAssigneeSelectPanelComponent, + this.viewContainerRef, injector)); + component.onDestroy(() => { + if (component.instance.userSelected) { + this.assignee = component.instance.result; + this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); + this.propagateChange(this.assignee?.id); + } + }); + } + } + +} 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 new file mode 100644 index 0000000000..a2a0c158a5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -0,0 +1,103 @@ + + + + + + + + + + +
+ +
+ + +
+
+
+ +
+
+ + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + +
+
+ + alarm.alarm-type-list + + + {{type}} + cancel + + + + +
+
+ + {{ 'alarm.search-propagated-alarms' | translate }} + + + {{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }} + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss similarity index 66% rename from ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss rename to ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss index b1abff0335..1c10e244b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -14,23 +14,22 @@ * limitations under the License. */ :host { - width: 100%; - height: 100%; - min-width: 300px; + display: block; overflow: hidden; - background: #fff; - border-radius: 4px; - box-shadow: - 0 7px 8px -4px rgba(0, 0, 0, .2), - 0 13px 19px 2px rgba(0, 0, 0, .14), - 0 5px 24px 4px rgba(0, 0, 0, .12); - - .mat-content { - overflow: hidden; - background-color: #fff; + max-width: 100%; + .mdc-button { + max-width: 100%; } +} - .mat-padding { - padding: 16px; +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } } } 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 new file mode 100644 index 0000000000..c6b8ee86e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -0,0 +1,318 @@ +/// +/// 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. +/// + +import { + Component, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; +import { coerceBoolean } from '@shared/decorators/coerce-boolean'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { + AlarmSearchStatus, + alarmSearchStatusTranslations, + 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'; + +export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmFilterConfigData'); + +export interface AlarmFilterConfigData { + panelMode: boolean; + userMode: boolean; + alarmFilterConfig: AlarmFilterConfig; +} + +// @dynamic +@Component({ + selector: 'tb-alarm-filter-config', + templateUrl: './alarm-filter-config.component.html', + styleUrls: ['./alarm-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmFilterConfigComponent), + multi: true + } + ] +}) +export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { + + @ViewChild('alarmFilterPanel') + alarmFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @coerceBoolean() + @Input() + userMode = false; + + @coerceBoolean() + @Input() + propagatedFilter = true; + + panelMode = false; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); + + alarmFilterConfigForm: UntypedFormGroup; + + alarmFilterOverlayRef: OverlayRef; + + panelResult: AlarmFilterConfig = null; + + private alarmFilterConfig: AlarmFilterConfig; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) + private data: AlarmFilterConfigData | undefined, + @Optional() + private overlayRef: OverlayRef, + private fb: UntypedFormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.userMode = this.data.userMode; + this.alarmFilterConfig = this.data.alarmFilterConfig; + } + this.alarmFilterConfigForm = this.fb.group({ + statusList: [null, []], + severityList: [null, []], + typeList: [null, []], + searchPropagatedAlarms: [false, []], + assignedToCurrentUser: [false, []], + assigneeId: [null, []] + }); + this.alarmFilterConfigForm.valueChanges.subscribe( + () => { + this.updateValidators(); + if (!this.buttonMode) { + this.alarmConfigUpdated(this.alarmFilterConfigForm.value); + } + } + ); + if (this.panelMode) { + this.updateAlarmConfigForm(this.alarmFilterConfig); + } + } + + ngOnDestroy(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmFilterConfigForm.disable({emitEvent: false}); + } else { + this.alarmFilterConfigForm.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(alarmFilterConfig?: AlarmFilterConfig): void { + this.alarmFilterConfig = alarmFilterConfig; + this.updateButtonDisplayValue(); + this.updateAlarmConfigForm(alarmFilterConfig); + } + + private updateValidators() { + const assignedToCurrentUser = this.alarmFilterConfigForm.get('assignedToCurrentUser').value; + if (assignedToCurrentUser) { + this.alarmFilterConfigForm.get('assigneeId').disable({emitEvent: false}); + } else { + this.alarmFilterConfigForm.get('assigneeId').enable({emitEvent: false}); + } + this.alarmFilterConfigForm.get('assigneeId').updateValueAndValidity({emitEvent: false}); + } + + toggleAlarmFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-alarm-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) + .withPositions([connectedPosition]); + + this.alarmFilterOverlayRef = this.overlay.create(config); + this.alarmFilterOverlayRef.backdropClick().subscribe(() => { + this.alarmFilterOverlayRef.dispose(); + }); + this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel, + this.viewContainerRef)); + } + + cancel() { + this.updateAlarmConfigForm(this.alarmFilterConfig); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.alarmFilterOverlayRef.dispose(); + } + } + + update() { + this.alarmConfigUpdated(this.alarmFilterConfigForm.value); + if (this.panelMode) { + this.panelResult = this.alarmFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.alarmFilterOverlayRef.dispose(); + } + } + + 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, + severityList: alarmFilterConfig?.severityList, + typeList: alarmFilterConfig?.typeList, + searchPropagatedAlarms: alarmFilterConfig?.searchPropagatedAlarms, + assignedToCurrentUser: alarmFilterConfig?.assignedToCurrentUser, + assigneeId: alarmFilterConfig?.assigneeId + }, {emitEvent: false}); + this.updateValidators(); + } + + private alarmConfigUpdated(alarmFilterConfig: AlarmFilterConfig) { + this.alarmFilterConfig = alarmFilterConfig; + this.updateButtonDisplayValue(); + this.propagateChange(this.alarmFilterConfig); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.alarmFilterConfig?.statusList?.length) { + filterTextParts.push(this.alarmFilterConfig.statusList.map(s => + this.translate.instant(alarmSearchStatusTranslations.get(s))).join(', ')); + } + if (this.alarmFilterConfig?.severityList?.length) { + filterTextParts.push(this.alarmFilterConfig.severityList.map(s => + this.translate.instant(alarmSeverityTranslations.get(s))).join(', ')); + } + if (this.alarmFilterConfig?.typeList?.length) { + filterTextParts.push(this.alarmFilterConfig.typeList.join(', ')); + } + if (this.alarmFilterConfig?.assignedToCurrentUser) { + filterTextParts.push(this.translate.instant('alarm.assigned-to-me')); + } else if (this.alarmFilterConfig?.assigneeId) { + filterTextParts.push(this.translate.instant('alarm.assigned')); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('alarm.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + +} 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 9b67da4b71..4e929986e6 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 @@ -32,6 +32,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { AlarmInfo, AlarmQuery, + AlarmQueryV2, AlarmSearchStatus, alarmSeverityColors, alarmSeverityTranslations, @@ -45,7 +46,7 @@ import { AlarmDetailsDialogComponent, AlarmDetailsDialogData } from '@home/components/alarm/alarm-details-dialog.component'; -import { DAY, historyInterval } from '@shared/models/time/time.models'; +import { DAY, forAllTimeInterval, historyInterval } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -60,21 +61,24 @@ import { import { ComponentPortal } from '@angular/cdk/portal'; import { isDefinedAndNotNull } from '@core/utils'; import { UtilsService } from '@core/services/utils.service'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; +import { EntityService } from '@core/http/entity.service'; export class AlarmTableConfig extends EntityTableConfig { private authUser = getCurrentAuthUser(this.store); - searchStatus: AlarmSearchStatus; + alarmFilterConfig: AlarmFilterConfig; constructor(private alarmService: AlarmService, + private entityService: EntityService, private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, private dialog: MatDialog, private alarmsMode: AlarmsMode = AlarmsMode.ALL, public entityId: EntityId = null, - private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, + private defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}, private store: Store, private viewContainerRef: ViewContainerRef, private overlay: Overlay, @@ -85,8 +89,9 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; + this.forAllTimeEnabled = true; this.pageMode = pageMode; - this.defaultTimewindowInterval = historyInterval(DAY * 30); + this.defaultTimewindowInterval = forAllTimeInterval(); this.detailsPanelEnabled = false; this.selectionEnabled = false; this.searchEnabled = true; @@ -97,7 +102,7 @@ export class AlarmTableConfig extends EntityTableConfig this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); this.entityResources = { } as EntityTypeResource; - this.searchStatus = defaultSearchStatus; + this.alarmFilterConfig = defaultAlarmFilterConfig; this.headerComponent = AlarmTableHeaderComponent; @@ -159,12 +164,13 @@ export class AlarmTableConfig extends EntityTableConfig } fetchAlarms(pageLink: TimePageLink): Observable> { - const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null); + const alarmFilter = this.entityService.resolveAlarmFilter(this.alarmFilterConfig); + const query = new AlarmQueryV2(this.entityId, pageLink, alarmFilter); switch (this.alarmsMode) { case AlarmsMode.ALL: - return this.alarmService.getAllAlarms(query); + return this.alarmService.getAllAlarmsV2(query); case AlarmsMode.ENTITY: - return this.alarmService.getAlarms(query); + return this.alarmService.getAlarmsV2(query); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html index d010d83a07..673386129e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html @@ -15,12 +15,8 @@ limitations under the License. --> - - alarm.alarm-status - - - {{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }} - - - + + diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss index 177ae129eb..202ad4a8ec 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss @@ -16,9 +16,7 @@ @import '../../../../../scss/constants'; :host { - /* flex: 1; - display: flex; - justify-content: flex-start; */ padding-right: 8px; - min-width: 160px; + overflow: hidden; + max-width: 100%; } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts index cbfeed4705..f25c086c27 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts @@ -20,6 +20,7 @@ import { AppState } from '@core/core.state'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models'; import { AlarmTableConfig } from './alarm-table-config'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; @Component({ selector: 'tb-alarm-table-header', @@ -28,11 +29,6 @@ import { AlarmTableConfig } from './alarm-table-config'; }) export class AlarmTableHeaderComponent extends EntityTableHeaderComponent { - alarmSearchStatusTranslationsMap = alarmSearchStatusTranslations; - - alarmSearchStatusTypes = Object.keys(AlarmSearchStatus); - alarmSearchStatusEnum = AlarmSearchStatus; - get alarmTableConfig(): AlarmTableConfig { return this.entitiesTableConfig as AlarmTableConfig; } @@ -41,8 +37,8 @@ export class AlarmTableHeaderComponent extends EntityTableHeaderComponent; + statusList?: Array; + severityList?: Array; + assignedToMe?: boolean; +} @Component({ selector: 'tb-alarm-table', @@ -59,7 +69,7 @@ export class AlarmTableComponent implements OnInit { set entityId(entityId: EntityId) { this.entityIdValue = entityId; if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) { - this.alarmTableConfig.searchStatus = AlarmSearchStatus.ANY; + this.alarmTableConfig.alarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}; this.alarmTableConfig.entityId = entityId; this.entitiesTable.resetSortAndFilter(this.activeValue); if (!this.activeValue) { @@ -73,6 +83,7 @@ export class AlarmTableComponent implements OnInit { alarmTableConfig: AlarmTableConfig; constructor(private alarmService: AlarmService, + private entityService: EntityService, private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, @@ -82,7 +93,8 @@ export class AlarmTableComponent implements OnInit { private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, private utilsService: UtilsService, - private route: ActivatedRoute) { + private route: ActivatedRoute, + private router: Router) { } ngOnInit() { @@ -94,15 +106,52 @@ export class AlarmTableComponent implements OnInit { if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) { this.alarmsMode = this.route.snapshot.data.alarmsMode; } + const defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]}; + if (pageMode) { + const routerQueryParams: AlarmPageQueryParams = this.route.snapshot.queryParams; + if (routerQueryParams) { + const queryParams = deepClone(routerQueryParams); + let replaceUrl = false; + if (routerQueryParams?.typeList) { + defaultAlarmFilterConfig.typeList = routerQueryParams?.typeList; + delete queryParams.typeList; + replaceUrl = true; + } + if (routerQueryParams?.statusList) { + defaultAlarmFilterConfig.statusList = routerQueryParams?.statusList; + delete queryParams.statusList; + replaceUrl = true; + } + if (routerQueryParams?.severityList) { + defaultAlarmFilterConfig.severityList = routerQueryParams?.severityList; + delete queryParams.severityList; + replaceUrl = true; + } + if (routerQueryParams?.assignedToMe) { + defaultAlarmFilterConfig.assignedToCurrentUser = routerQueryParams?.assignedToMe; + delete queryParams.assignedToMe; + replaceUrl = true; + } + if (replaceUrl) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: '', + replaceUrl: true + }); + } + } + } this.alarmTableConfig = new AlarmTableConfig( this.alarmService, + this.entityService, this.dialogService, this.translate, this.datePipe, this.dialog, this.alarmsMode, this.entityIdValue, - AlarmSearchStatus.ANY, + defaultAlarmFilterConfig, this.store, this.viewContainerRef, this.overlay, diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 21fcd0efca..0a2c5194f8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -107,7 +107,8 @@ export class EntityAliasesDialogComponent extends DialogComponent { - if ([DatasourceType.entity, DatasourceType.entityCount].includes(datasource.type) && datasource.entityAliasId) { + if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type) + && datasource.entityAliasId) { this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); } }); diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5459311c6a..f07010854c 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -42,7 +42,7 @@ + asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled">
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss index f90f9c9f8f..37fb5d8ad9 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss @@ -69,6 +69,9 @@ .mat-toolbar { height: auto; min-height: 100px; + .mat-toolbar-tools{ + height: auto; + } .tb-entity-table-title{ padding-bottom: 5px; diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index be2b6f97a4..b868fd1e47 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -254,9 +254,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa if (this.entitiesTableConfig.useTimePageLink) { this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; - const currentTime = Date.now(); + const interval = this.getTimePageLinkInterval(); this.pageLink = new TimePageLink(10, 0, null, sortOrder, - currentTime - this.timewindow.history.timewindowMs, currentTime); + interval.startTime, interval.endTime); } else { this.pageLink = new PageLink(10, 0, null, sortOrder); } @@ -411,18 +411,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } if (this.entitiesTableConfig.useTimePageLink) { const timePageLink = this.pageLink as TimePageLink; - if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { - const currentTime = Date.now(); - timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; - timePageLink.endTime = currentTime; - } else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) { - const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval); - timePageLink.startTime = startEndTime[0]; - timePageLink.endTime = startEndTime[1]; - } else { - timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; - timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; - } + const interval = this.getTimePageLinkInterval(); + timePageLink.startTime = interval.startTime; + timePageLink.endTime = interval.endTime; } this.dataSource.loadEntities(this.pageLink); if (this.isDetailsOpen && this.entityDetailsPanel) { @@ -430,6 +421,31 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } + private getTimePageLinkInterval(): {startTime?: number, endTime?: number} { + const interval: {startTime?: number, endTime?: number} = {}; + switch (this.timewindow.history.historyType) { + case HistoryWindowType.LAST_INTERVAL: + const currentTime = Date.now(); + interval.startTime = currentTime - this.timewindow.history.timewindowMs; + interval.endTime = currentTime; + break; + case HistoryWindowType.FIXED: + interval.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; + interval.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; + break; + case HistoryWindowType.INTERVAL: + const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval); + interval.startTime = startEndTime[0]; + interval.endTime = startEndTime[1]; + break; + case HistoryWindowType.FOR_ALL_TIME: + interval.startTime = null; + interval.endTime = null; + break; + } + return interval; + } + private dataLoaded(col?: number, row?: number) { if (isFinite(col) && isFinite(row)) { this.clearCellCache(col, row); diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 7a91645e31..467d006aab 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -178,6 +178,9 @@ import { modulesMap } from '@modules/common/modules-map'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; +import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; +import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; +import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; @NgModule({ declarations: @@ -202,6 +205,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s AlarmTableHeaderComponent, AlarmTableComponent, AlarmAssigneePanelComponent, + AlarmAssigneeSelectComponent, + AlarmAssigneeSelectPanelComponent, + AlarmFilterConfigComponent, AttributeTableComponent, AddAttributeDialogComponent, EditAttributeValuePanelComponent, @@ -352,6 +358,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s RelationFiltersComponent, AlarmTableComponent, AlarmAssigneePanelComponent, + AlarmAssigneeSelectComponent, + AlarmAssigneeSelectPanelComponent, + AlarmFilterConfigComponent, AttributeTableComponent, AliasesEntitySelectComponent, AliasesEntityAutocompleteComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html index 7d20055f6e..0230c4ae08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html @@ -37,7 +37,7 @@ dndType="{{ dndId }}" dndEffectAllowed="move" dndDraggingSourceClass="tb-chip-dragging" - [removable]="!disabled && !isEntityCountDatasource" + [removable]="!disabled && !isCountDatasource" (removed)="remove(key)">
@@ -78,7 +78,7 @@ (click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-32"> edit - close + close
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index 757f838fe4..23051d78ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -102,7 +102,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange private maxDataKeysValue: number; get maxDataKeys(): number { - return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; + return this.isCountDatasource ? 1 : this.maxDataKeysValue; } @Input() @@ -136,7 +136,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange private requiredValue: boolean; get required(): boolean { - return this.requiredValue || !this.optDataKeys || this.isEntityCountDatasource; + return this.requiredValue || !this.optDataKeys || this.isCountDatasource; } @Input() set required(value: boolean) { @@ -240,7 +240,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.datasourceType === DatasourceType.function) { return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); - } else if (!this.isEntityCountDatasource) { + } else if (!this.isCountDatasource) { return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); } else { return ''; @@ -287,7 +287,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange private reset() { if (this.widgetType === widgetType.alarm) { this.keys = this.utils.getDefaultAlarmDataKeys(); - } else if (this.isEntityCountDatasource) { + } else if (this.isCountDatasource) { this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)]; } else { this.keys = []; @@ -535,8 +535,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange }, 0); } - get isEntityCountDatasource(): boolean { - return this.datasourceType === DatasourceType.entityCount; + get isCountDatasource(): boolean { + return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); } private clearSearchCache() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html deleted file mode 100644 index 73d76ee07b..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html +++ /dev/null @@ -1,65 +0,0 @@ - -
- - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - - - - alarm.alarm-type-list - - - {{type}} - cancel - - - - -
- - -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts deleted file mode 100644 index 3f3f1c2f85..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts +++ /dev/null @@ -1,119 +0,0 @@ -/// -/// 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. -/// - -import { Component, Inject, InjectionToken } from '@angular/core'; -import { - AlarmSearchStatus, - alarmSearchStatusTranslations, - AlarmSeverity, - alarmSeverityTranslations -} from '@shared/models/alarm.models'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { OverlayRef } from '@angular/cdk/overlay'; - -export const ALARM_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); - -export interface AlarmFilterPanelData { - statusList: AlarmSearchStatus[]; - severityList: AlarmSeverity[]; - typeList: string[]; -} - -@Component({ - selector: 'tb-alarm-filter-panel', - templateUrl: './alarm-filter-panel.component.html', - styleUrls: ['./alarm-filter-panel.component.scss'] -}) -export class AlarmFilterPanelComponent { - - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - - alarmFilterFormGroup: UntypedFormGroup; - - result: AlarmFilterPanelData; - - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, - AlarmSearchStatus.CLEARED, - AlarmSearchStatus.ACK, - AlarmSearchStatus.UNACK]; - - alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; - - alarmSeverities = Object.keys(AlarmSeverity); - alarmSeverityEnum = AlarmSeverity; - - alarmSeverityTranslationMap = alarmSeverityTranslations; - - constructor(@Inject(ALARM_FILTER_PANEL_DATA) - public data: AlarmFilterPanelData, - public overlayRef: OverlayRef, - private fb: UntypedFormBuilder) { - this.alarmFilterFormGroup = this.fb.group( - { - alarmStatusList: [this.data.statusList], - alarmSeverityList: [this.data.severityList], - alarmTypeList: [this.data.typeList] - } - ); - } - - public alarmTypeList(): string[] { - return this.alarmFilterFormGroup.get('alarmTypeList').value; - } - - public removeAlarmType(type: string): void { - const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); - this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.chipInput.inputElement; - const value = event.value; - - const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; - - if ((value || '').trim()) { - types.push(value.trim()); - this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); - this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - - update() { - this.result = { - statusList: this.alarmFilterFormGroup.get('alarmStatusList').value, - severityList: this.alarmFilterFormGroup.get('alarmSeverityList').value, - typeList: this.alarmFilterFormGroup.get('alarmTypeList').value - }; - this.overlayRef.dispose(); - } - - cancel() { - this.overlayRef.dispose(); - } -} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 8f3a37c712..abf43a8926 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -41,6 +41,7 @@ import { deepClone, hashCode, isDefined, + isDefinedAndNotNull, isNumber, isObject, isUndefined @@ -73,9 +74,9 @@ import { getColumnDefaultVisibility, getColumnSelectionAvailability, getColumnWidth, + getHeaderTitle, getRowStyleInfo, getTableCellButtonActions, - getHeaderTitle, noDataMessage, prepareTableCellButtonActions, RowStyleInfo, @@ -93,8 +94,8 @@ import { } from '@home/components/widget/lib/display-columns-panel.component'; import { AlarmDataInfo, - alarmFields, AlarmInfo, - AlarmSearchStatus, + alarmFields, + AlarmInfo, alarmSeverityColors, alarmSeverityTranslations, AlarmStatus, @@ -118,23 +119,25 @@ import { KeyFilter } from '@app/shared/models/query/query.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { - ALARM_FILTER_PANEL_DATA, - AlarmFilterPanelComponent, - AlarmFilterPanelData -} from '@home/components/widget/lib/alarm-filter-panel.component'; import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; import { - ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, + ALARM_ASSIGNEE_PANEL_DATA, + AlarmAssigneePanelComponent, AlarmAssigneePanelData } from '@home/components/alarm/alarm-assignee-panel.component'; import { AlarmCommentDialogComponent, AlarmCommentDialogData } from '@home/components/alarm/alarm-comment-dialog.component'; +import { EntityService } from '@core/http/entity.service'; +import { + ALARM_FILTER_CONFIG_DATA, + AlarmFilterConfigComponent, + AlarmFilterConfigData +} from '@home/components/alarm/alarm-filter-config.component'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -254,6 +257,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private datePipe: DatePipe, private dialog: MatDialog, private dialogService: DialogService, + private entityService: EntityService, private alarmService: AlarmService, private cd: ChangeDetectorRef) { super(store); @@ -378,17 +382,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; - this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms) - ? this.widgetConfig.searchPropagatedAlarms : true; - let alarmStatusList: AlarmSearchStatus[] = []; - if (isDefined(this.widgetConfig.alarmStatusList) && this.widgetConfig.alarmStatusList.length) { - alarmStatusList = this.widgetConfig.alarmStatusList; - } else if (isDefined(this.widgetConfig.alarmSearchStatus) && this.widgetConfig.alarmSearchStatus !== AlarmSearchStatus.ANY) { - alarmStatusList = [this.widgetConfig.alarmSearchStatus]; - } - this.pageLink.statusList = alarmStatusList; - this.pageLink.severityList = isDefined(this.widgetConfig.alarmSeverityList) ? this.widgetConfig.alarmSeverityList : []; - this.pageLink.typeList = isDefined(this.widgetConfig.alarmTypeList) ? this.widgetConfig.alarmTypeList : []; + const alarmFilter = this.entityService.resolveAlarmFilter(this.widgetConfig.alarmFilterConfig); + this.pageLink = {...this.pageLink, ...alarmFilter}; this.noDataDisplayMessageText = noDataMessage(this.widgetConfig.noDataDisplayMessage, 'alarm.no-alarms-prompt', this.utils, this.translate); @@ -596,6 +591,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, const target = $event.target || $event.srcElement || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.panelClass = 'tb-alarm-filter-panel'; config.hasBackdrop = true; const connectedPosition: ConnectedPosition = { originX: 'end', @@ -612,12 +608,18 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, }); const providers: StaticProvider[] = [ { - provide: ALARM_FILTER_PANEL_DATA, + provide: ALARM_FILTER_CONFIG_DATA, useValue: { - statusList: this.pageLink.statusList, - severityList: this.pageLink.severityList, - typeList: this.pageLink.typeList - } as AlarmFilterPanelData + panelMode: true, + userMode: true, + alarmFilterConfig: { + statusList: deepClone(this.pageLink.statusList), + severityList: deepClone(this.pageLink.severityList), + typeList: deepClone(this.pageLink.typeList), + searchPropagatedAlarms: this.pageLink.searchPropagatedAlarms, + assignedToCurrentUser: isDefinedAndNotNull(this.pageLink.assigneeId) + } + } as AlarmFilterConfigData }, { provide: OverlayRef, @@ -625,14 +627,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } ]; const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); - const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent, + const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterConfigComponent, this.viewContainerRef, injector)); componentRef.onDestroy(() => { - if (componentRef.instance.result) { - const result = componentRef.instance.result; - this.pageLink.statusList = result.statusList; - this.pageLink.severityList = result.severityList; - this.pageLink.typeList = result.typeList; + if (componentRef.instance.panelResult) { + const result = componentRef.instance.panelResult; + const alarmFilter = this.entityService.resolveAlarmFilter(result); + this.pageLink = {...this.pageLink, ...alarmFilter}; this.resetPageIndex(); this.updateData(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 61b94a37a6..9d3b6a737b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -20,7 +20,6 @@ import { SharedModule } from '@app/shared/shared.module'; import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component'; import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component'; -import { AlarmFilterPanelComponent } from '@home/components/widget/lib/alarm-filter-panel.component'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component'; @@ -49,7 +48,6 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; declarations: [ DisplayColumnsPanelComponent, - AlarmFilterPanelComponent, EntitiesTableWidgetComponent, AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, @@ -75,25 +73,25 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; HomePageWidgetsModule, SharedHomeComponentsModule ], - exports: [ - EntitiesTableWidgetComponent, - AlarmsTableWidgetComponent, - TimeseriesTableWidgetComponent, - EntitiesHierarchyWidgetComponent, - EdgesOverviewWidgetComponent, - RpcWidgetsModule, - HomePageWidgetsModule, - DateRangeNavigatorWidgetComponent, - JsonInputWidgetComponent, - MultipleInputWidgetComponent, - TripAnimationComponent, - PhotoCameraInputWidgetComponent, - GatewayFormComponent, - NavigationCardsWidgetComponent, - NavigationCardWidgetComponent, - QrCodeWidgetComponent, - MarkdownWidgetComponent - ], + exports: [ + EntitiesTableWidgetComponent, + AlarmsTableWidgetComponent, + TimeseriesTableWidgetComponent, + EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, + RpcWidgetsModule, + HomePageWidgetsModule, + DateRangeNavigatorWidgetComponent, + JsonInputWidgetComponent, + MultipleInputWidgetComponent, + TripAnimationComponent, + PhotoCameraInputWidgetComponent, + GatewayFormComponent, + NavigationCardsWidgetComponent, + NavigationCardWidgetComponent, + QrCodeWidgetComponent, + MarkdownWidgetComponent + ], providers: [ CustomDialogService, ImportExportService, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index b16322bea5..df7dd942a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -42,47 +42,7 @@
-
- - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} - - - -
-
- - alarm.alarm-type-list - - - {{type}} - cancel - - - - - - {{ 'alarm.search-propagated-alarms' | translate }} - -
+
+ datasourceControl.get('type').value === datasourceType.entityCount || + datasourceControl.get('type').value === datasourceType.alarmCount ? datasourceControl.get('type').value : ''"> + - = 0) { - types.splice(index, 1); - this.dataSettings.get('alarmTypeList').setValue(types); - this.dataSettings.get('alarmTypeList').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.chipInput.inputElement; - const value = event.value; - - const types: string[] = this.dataSettings.get('alarmTypeList').value; - - if ((value || '').trim()) { - types.push(value.trim()); - this.dataSettings.get('alarmTypeList').setValue(types); - this.dataSettings.get('alarmTypeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - public displayAdvanced(): boolean { return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 1cce7d2899..e7caba0e3c 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -145,6 +145,7 @@ export class EntityTableConfig, P extends PageLink = P loadDataOnInit = true; onLoadAction: (route: ActivatedRoute) => void = null; useTimePageLink = false; + forAllTimeEnabled = false; defaultTimewindowInterval = historyInterval(DAY); entityType: EntityType = null; tableTitle = ''; diff --git a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw index 34b097fe75..b52a79942f 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw +++ b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw @@ -687,18 +687,18 @@ "config": { "datasources": [ { - "type": "entityCount", + "type": "alarmCount", "name": null, - "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "entityAliasId": null, "filterId": null, "dataKeys": [ { "name": "count", "type": "count", "label": "totalAlarms", - "color": "#2196f3", + "color": "#ffc107", "settings": {}, - "_hash": 0.8491768696709192, + "_hash": 0.8743321483507485, "aggregationType": null, "units": null, "decimals": null, @@ -706,21 +706,26 @@ "usePostProcessing": null, "postFuncBody": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } }, { - "type": "entityCount", + "type": "alarmCount", "name": null, - "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", - "filterId": "42f42812-92fe-a13a-b0ba-c6e0dfab3584", + "entityAliasId": null, + "filterId": null, "dataKeys": [ { "name": "count", "type": "count", "label": "assignedToMeAlarms", - "color": "#4caf50", + "color": "#ffc107", "settings": {}, - "_hash": 0.1262449138010293, + "_hash": 0.6392390736755882, "aggregationType": null, "units": null, "decimals": null, @@ -728,21 +733,27 @@ "usePostProcessing": null, "postFuncBody": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ], + "assignedToCurrentUser": true + } }, { - "type": "entityCount", + "type": "alarmCount", "name": null, - "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", - "filterId": "39d92c21-2426-7346-4484-452829146c33", + "entityAliasId": null, + "filterId": null, "dataKeys": [ { "name": "count", "type": "count", "label": "criticalAlarms", - "color": "#f44336", + "color": "#ffc107", "settings": {}, - "_hash": 0.39119172615806797, + "_hash": 0.8329478098552843, "aggregationType": null, "units": null, "decimals": null, @@ -750,7 +761,15 @@ "usePostProcessing": null, "postFuncBody": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ], + "severityList": [ + "CRITICAL" + ] + } } ], "timewindow": { @@ -783,7 +802,7 @@ "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n" }, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 34d1909e38..0524f7e989 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -98,6 +98,11 @@
+ +
+ timewindow.for-all-time +
+
timewindow.last diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 171a692e44..f4a2e8b916 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -35,6 +35,7 @@ import { OverlayRef } from '@angular/cdk/overlay'; export interface TimewindowPanelData { historyOnly: boolean; + forAllTimeEnabled: boolean; quickIntervalOnly: boolean; timewindow: Timewindow; aggregation: boolean; @@ -53,6 +54,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { historyOnly = false; + forAllTimeEnabled = false; + quickIntervalOnly = false; aggregation = false; @@ -87,6 +90,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { public viewContainerRef: ViewContainerRef) { super(store); this.historyOnly = data.historyOnly; + this.forAllTimeEnabled = data.forAllTimeEnabled; this.quickIntervalOnly = data.quickIntervalOnly; this.timewindow = data.timewindow; this.aggregation = data.aggregation; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.html b/ui-ngx/src/app/shared/components/time/timewindow.component.html index 2b689831a9..a9d5ce0eaa 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - +
; severityList?: Array; searchPropagatedAlarms?: boolean; + assigneeId?: UserId; } +export interface AlarmFilterConfig extends AlarmFilter { + assignedToCurrentUser?: boolean; +} + +export type AlarmCountQuery = EntityCountQuery & AlarmFilter; + +export type AlarmDataPageLink = EntityDataPageLink & AlarmFilter; + export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection { if (pageLink.sortOrder) { return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection; @@ -843,7 +853,8 @@ export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: E }; datasource.entityId = entity.id; datasource.entityType = entity.entityType; - if (datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) { + if (datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount + || datasource.type === DatasourceType.alarmCount) { if (datasource.type === DatasourceType.entity) { datasource.entityName = entity.name; datasource.entityLabel = entity.label; diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 17a7031099..50cbef2f8b 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -22,6 +22,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { map } from 'rxjs/operators'; import { NgZone } from '@angular/core'; import { + AlarmCountQuery, AlarmData, AlarmDataQuery, EntityCountQuery, EntityData, @@ -224,6 +225,11 @@ export class AlarmDataCmd implements WebsocketCmd { } } +export class AlarmCountCmd implements WebsocketCmd { + cmdId: number; + query?: AlarmCountQuery; +} + export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } @@ -236,6 +242,10 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } +export class AlarmCountUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + export class TelemetryPluginCmdsWrapper implements CmdWrapper { constructor() { @@ -248,6 +258,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper { this.alarmDataUnsubscribeCmds = []; this.entityCountCmds = []; this.entityCountUnsubscribeCmds = []; + this.alarmCountCmds = []; + this.alarmCountUnsubscribeCmds = []; } attrSubCmds: Array; tsSubCmds: Array; @@ -258,6 +270,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper { alarmDataUnsubscribeCmds: Array; entityCountCmds: Array; entityCountUnsubscribeCmds: Array; + alarmCountCmds: Array; + alarmCountUnsubscribeCmds: Array; private static popCmds(cmds: Array, leftCount: number): Array { const toPublish = Math.min(cmds.length, leftCount); @@ -277,7 +291,9 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper { this.alarmDataCmds.length > 0 || this.alarmDataUnsubscribeCmds.length > 0 || this.entityCountCmds.length > 0 || - this.entityCountUnsubscribeCmds.length > 0; + this.entityCountUnsubscribeCmds.length > 0 || + this.alarmCountCmds.length > 0 || + this.alarmCountUnsubscribeCmds.length > 0; } public clear() { @@ -290,6 +306,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper { this.alarmDataUnsubscribeCmds.length = 0; this.entityCountCmds.length = 0; this.entityCountUnsubscribeCmds.length = 0; + this.alarmCountCmds.length = 0; + this.alarmCountUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -312,6 +330,10 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper { preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount); leftCount -= preparedWrapper.entityCountCmds.length; preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.entityCountUnsubscribeCmds.length; + preparedWrapper.alarmCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmCountCmds, leftCount); + leftCount -= preparedWrapper.alarmCountCmds.length; + preparedWrapper.alarmCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmCountUnsubscribeCmds, leftCount); return preparedWrapper; } } @@ -337,6 +359,7 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { export enum CmdUpdateType { ENTITY_DATA = 'ENTITY_DATA', ALARM_DATA = 'ALARM_DATA', + ALARM_COUNT_DATA = 'ALARM_COUNT_DATA', COUNT_DATA = 'COUNT_DATA', NOTIFICATIONS_COUNT = 'NOTIFICATIONS_COUNT', NOTIFICATIONS = 'NOTIFICATIONS' @@ -369,7 +392,13 @@ export interface EntityCountUpdateMsg extends CmdUpdateMsg { count: number; } -export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; +export interface AlarmCountUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.ALARM_COUNT_DATA; + count: number; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | AlarmCountUpdateMsg | + EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; export const isEntityDataUpdateMsg = (message: WebsocketDataMsg): message is EntityDataUpdateMsg => { const updateMsg = (message as CmdUpdateMsg); @@ -386,6 +415,11 @@ export const isEntityCountUpdateMsg = (message: WebsocketDataMsg): message is En return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA; }; +export const isAlarmCountUpdateMsg = (message: WebsocketDataMsg): message is AlarmCountUpdateMsg => { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_COUNT_DATA; +}; + export class SubscriptionUpdate implements SubscriptionUpdateMsg { subscriptionId: number; errorCode: number; @@ -566,18 +600,29 @@ export class EntityCountUpdate extends CmdUpdate { } } +export class AlarmCountUpdate extends CmdUpdate { + count: number; + + constructor(msg: AlarmCountUpdateMsg) { + super(msg); + this.count = msg.count; + } +} + export class TelemetrySubscriber extends WsSubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); private alarmDataSubject = new ReplaySubject(1); private entityCountSubject = new ReplaySubject(1); + private alarmCountSubject = new ReplaySubject(1); private tsOffset = undefined; public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable(); public entityCount$ = this.entityCountSubject.asObservable(); + public alarmCount$ = this.alarmCountSubject.asObservable(); public static createEntityAttributesSubscription(telemetryService: TelemetryWebsocketService, entityId: EntityId, attributeScope: TelemetryType, @@ -629,6 +674,7 @@ export class TelemetrySubscriber extends WsSubscriber { this.entityDataSubject.complete(); this.alarmDataSubject.complete(); this.entityCountSubject.complete(); + this.alarmCountSubject.complete(); super.complete(); } @@ -706,6 +752,18 @@ export class TelemetrySubscriber extends WsSubscriber { } } + public onAlarmCount(message: AlarmCountUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.alarmCountSubject.next(message); + } + ); + } else { + this.alarmCountSubject.next(message); + } + } + public attributeData$(): Observable> { const attributeData = new Array(); return this.data$.pipe( diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index ae0cfe0b34..3e87150f87 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -43,7 +43,8 @@ export enum RealtimeWindowType { export enum HistoryWindowType { LAST_INTERVAL, FIXED, - INTERVAL + INTERVAL, + FOR_ALL_TIME } export interface IntervalWindow { @@ -187,17 +188,22 @@ export const QuickTimeIntervalTranslationMap = new Map ({ + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.FOR_ALL_TIME + } +}); -export function defaultTimewindow(timeService: TimeService): Timewindow { +export const historyInterval = (timewindowMs: number): Timewindow => ({ + selectedTab: TimewindowType.HISTORY, + history: { + historyType: HistoryWindowType.LAST_INTERVAL, + timewindowMs + } +}); + +export const defaultTimewindow = (timeService: TimeService): Timewindow => { const currentTime = moment().valueOf(); return { displayValue: '', @@ -229,17 +235,17 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { limit: Math.floor(timeService.getMaxDatapointsLimit() / 2) } }; -} +}; -function getTimewindowType(timewindow: Timewindow): TimewindowType { +const getTimewindowType = (timewindow: Timewindow): TimewindowType => { if (isUndefined(timewindow.selectedTab)) { return isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; } else { return timewindow.selectedTab; } -} +}; -export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow { +export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow => { const model = defaultTimewindow(timeService); if (value) { model.hideInterval = value.hideInterval; @@ -311,10 +317,10 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO model.realtime.realtimeType = RealtimeWindowType.INTERVAL; } return model; -} +}; -export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, endTimeMs: number, - interval: number, timeService: TimeService): Timewindow { +export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, + interval: number, timeService: TimeService): Timewindow => { if (timewindow.history) { interval = isDefined(interval) ? interval : timewindow.history.interval; } else if (timewindow.realtime) { @@ -353,18 +359,20 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, }, timezone: timewindow.timezone }; -} +}; -export function timewindowTypeChanged(newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean { +export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean => { if (!newTimewindow || !oldTimewindow) { return false; } const newType = getTimewindowType(newTimewindow); const oldType = getTimewindowType(oldTimewindow); return newType !== oldType; -} +}; + +export const getTimezone = (tz: string): moment_.Moment => moment.tz(tz); -export function calculateTsOffset(timezone?: string): number { +export const calculateTsOffset = (timezone?: string): number => { if (timezone) { const tz = getTimezone(timezone); const localOffset = moment().utcOffset(); @@ -372,115 +380,19 @@ export function calculateTsOffset(timezone?: string): number { } else { return 0; } -} +}; -export function isHistoryTypeTimewindow(timewindow: Timewindow): boolean { - return getTimewindowType(timewindow) === TimewindowType.HISTORY; -} +export const isHistoryTypeTimewindow = (timewindow: Timewindow): boolean => getTimewindowType(timewindow) === TimewindowType.HISTORY; -export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, - timeService: TimeService): SubscriptionTimewindow { - const subscriptionTimewindow: SubscriptionTimewindow = { - fixedWindow: null, - realtimeWindowMs: null, - aggregation: { - interval: SECOND, - limit: timeService.getMaxDatapointsLimit(), - type: AggregationType.AVG - }, - timezone: timewindow.timezone, - tsOffset: calculateTsOffset(timewindow.timezone) - }; - let aggTimewindow; - if (stateData) { - subscriptionTimewindow.aggregation.type = AggregationType.NONE; - subscriptionTimewindow.aggregation.stateData = true; - } - if (isDefined(timewindow.aggregation) && !stateData) { - subscriptionTimewindow.aggregation = { - type: timewindow.aggregation.type || AggregationType.AVG, - limit: timewindow.aggregation.limit || timeService.getMaxDatapointsLimit() - }; - } - const selectedTab = getTimewindowType(timewindow); - if (selectedTab === TimewindowType.REALTIME) { - let realtimeType = timewindow.realtime.realtimeType; - if (isUndefined(realtimeType)) { - if (isDefined(timewindow.realtime.quickInterval)) { - realtimeType = RealtimeWindowType.INTERVAL; - } else { - realtimeType = RealtimeWindowType.LAST_INTERVAL; - } - } - if (realtimeType === RealtimeWindowType.INTERVAL) { - subscriptionTimewindow.realtimeWindowMs = - getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, timewindow.timezone); - subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval; - const currentDate = getCurrentTime(timewindow.timezone); - subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate).valueOf(); - } else { - subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; - const currentDate = getCurrentTime(timewindow.timezone); - subscriptionTimewindow.startTs = currentDate.valueOf() + stDiff - subscriptionTimewindow.realtimeWindowMs; - } - subscriptionTimewindow.aggregation.interval = - timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, - subscriptionTimewindow.aggregation.type); - aggTimewindow = subscriptionTimewindow.realtimeWindowMs; - if (realtimeType !== RealtimeWindowType.INTERVAL) { - const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; - if (startDiff) { - subscriptionTimewindow.startTs -= startDiff; - aggTimewindow += subscriptionTimewindow.aggregation.interval; - } - } +export const getCurrentTime = (tz?: string): moment_.Moment => { + if (tz) { + return moment().tz(tz); } else { - let historyType = timewindow.history.historyType; - if (isUndefined(historyType)) { - if (isDefined(timewindow.history.timewindowMs)) { - historyType = HistoryWindowType.LAST_INTERVAL; - } else if (isDefined(timewindow.history.quickInterval)) { - historyType = HistoryWindowType.INTERVAL; - } else { - historyType = HistoryWindowType.FIXED; - } - } - if (historyType === HistoryWindowType.LAST_INTERVAL) { - const currentDate = getCurrentTime(timewindow.timezone); - const currentTime = currentDate.valueOf(); - subscriptionTimewindow.fixedWindow = { - startTimeMs: currentTime - timewindow.history.timewindowMs, - endTimeMs: currentTime - }; - aggTimewindow = timewindow.history.timewindowMs; - } else if (historyType === HistoryWindowType.INTERVAL) { - const startEndTime = calculateIntervalStartEndTime(timewindow.history.quickInterval, timewindow.timezone); - subscriptionTimewindow.fixedWindow = { - startTimeMs: startEndTime[0], - endTimeMs: startEndTime[1] - }; - aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; - subscriptionTimewindow.quickInterval = timewindow.history.quickInterval; - } else { - subscriptionTimewindow.fixedWindow = { - startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset, - endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset - }; - aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; - } - subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; - subscriptionTimewindow.aggregation.interval = - timeService.boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type); - } - const aggregation = subscriptionTimewindow.aggregation; - aggregation.timeWindow = aggTimewindow; - if (aggregation.type !== AggregationType.NONE) { - aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); + return moment(); } - return subscriptionTimewindow; -} +}; -function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, tz?: string): number { +const getSubscriptionRealtimeWindowFromTimeInterval = (interval: QuickTimeInterval, tz?: string): number => { let currentDate; switch (interval) { case QuickTimeInterval.CURRENT_HOUR: @@ -514,19 +426,9 @@ function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterv currentDate = getCurrentTime(tz); return currentDate.endOf('year').diff(currentDate.clone().startOf('year')); } -} - -export function calculateIntervalStartEndTime(interval: QuickTimeInterval, tz?: string): [number, number] { - const startEndTs: [number, number] = [0, 0]; - const currentDate = getCurrentTime(tz); - const startDate = calculateIntervalStartTime(interval, currentDate); - startEndTs[0] = startDate.valueOf(); - const endDate = calculateIntervalEndTime(interval, startDate, tz); - startEndTs[1] = endDate.valueOf(); - return startEndTs; -} +}; -export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment): moment_.Moment { +export const calculateIntervalStartTime = (interval: QuickTimeInterval, currentDate: moment_.Moment): moment_.Moment => { switch (interval) { case QuickTimeInterval.YESTERDAY: currentDate.subtract(1, 'days'); @@ -577,9 +479,9 @@ export function calculateIntervalStartTime(interval: QuickTimeInterval, currentD case QuickTimeInterval.CURRENT_YEAR_SO_FAR: return currentDate.startOf('year'); } -} +}; -export function calculateIntervalEndTime(interval: QuickTimeInterval, startDate: moment_.Moment, tz?: string): number { +export const calculateIntervalEndTime = (interval: QuickTimeInterval, startDate: moment_.Moment, tz?: string): number => { switch (interval) { case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.DAY_BEFORE_YESTERDAY: @@ -614,9 +516,121 @@ export function calculateIntervalEndTime(interval: QuickTimeInterval, startDate: case QuickTimeInterval.CURRENT_YEAR_SO_FAR: return getCurrentTime(tz).valueOf(); } -} +}; -export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number { +export const calculateIntervalStartEndTime = (interval: QuickTimeInterval, tz?: string): [number, number] => { + const startEndTs: [number, number] = [0, 0]; + const currentDate = getCurrentTime(tz); + const startDate = calculateIntervalStartTime(interval, currentDate); + startEndTs[0] = startDate.valueOf(); + const endDate = calculateIntervalEndTime(interval, startDate, tz); + startEndTs[1] = endDate.valueOf(); + return startEndTs; +}; + +export const createSubscriptionTimewindow = (timewindow: Timewindow, stDiff: number, stateData: boolean, + timeService: TimeService): SubscriptionTimewindow => { + const subscriptionTimewindow: SubscriptionTimewindow = { + fixedWindow: null, + realtimeWindowMs: null, + aggregation: { + interval: SECOND, + limit: timeService.getMaxDatapointsLimit(), + type: AggregationType.AVG + }, + timezone: timewindow.timezone, + tsOffset: calculateTsOffset(timewindow.timezone) + }; + let aggTimewindow; + if (stateData) { + subscriptionTimewindow.aggregation.type = AggregationType.NONE; + subscriptionTimewindow.aggregation.stateData = true; + } + if (isDefined(timewindow.aggregation) && !stateData) { + subscriptionTimewindow.aggregation = { + type: timewindow.aggregation.type || AggregationType.AVG, + limit: timewindow.aggregation.limit || timeService.getMaxDatapointsLimit() + }; + } + const selectedTab = getTimewindowType(timewindow); + if (selectedTab === TimewindowType.REALTIME) { + let realtimeType = timewindow.realtime.realtimeType; + if (isUndefined(realtimeType)) { + if (isDefined(timewindow.realtime.quickInterval)) { + realtimeType = RealtimeWindowType.INTERVAL; + } else { + realtimeType = RealtimeWindowType.LAST_INTERVAL; + } + } + if (realtimeType === RealtimeWindowType.INTERVAL) { + subscriptionTimewindow.realtimeWindowMs = + getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, timewindow.timezone); + subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval; + const currentDate = getCurrentTime(timewindow.timezone); + subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate).valueOf(); + } else { + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; + const currentDate = getCurrentTime(timewindow.timezone); + subscriptionTimewindow.startTs = currentDate.valueOf() + stDiff - subscriptionTimewindow.realtimeWindowMs; + } + subscriptionTimewindow.aggregation.interval = + timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, + subscriptionTimewindow.aggregation.type); + aggTimewindow = subscriptionTimewindow.realtimeWindowMs; + if (realtimeType !== RealtimeWindowType.INTERVAL) { + const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; + if (startDiff) { + subscriptionTimewindow.startTs -= startDiff; + aggTimewindow += subscriptionTimewindow.aggregation.interval; + } + } + } else { + let historyType = timewindow.history.historyType; + if (isUndefined(historyType)) { + if (isDefined(timewindow.history.timewindowMs)) { + historyType = HistoryWindowType.LAST_INTERVAL; + } else if (isDefined(timewindow.history.quickInterval)) { + historyType = HistoryWindowType.INTERVAL; + } else { + historyType = HistoryWindowType.FIXED; + } + } + if (historyType === HistoryWindowType.LAST_INTERVAL) { + const currentDate = getCurrentTime(timewindow.timezone); + const currentTime = currentDate.valueOf(); + subscriptionTimewindow.fixedWindow = { + startTimeMs: currentTime - timewindow.history.timewindowMs, + endTimeMs: currentTime + }; + aggTimewindow = timewindow.history.timewindowMs; + } else if (historyType === HistoryWindowType.INTERVAL) { + const startEndTime = calculateIntervalStartEndTime(timewindow.history.quickInterval, timewindow.timezone); + subscriptionTimewindow.fixedWindow = { + startTimeMs: startEndTime[0], + endTimeMs: startEndTime[1] + }; + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval; + } else { + subscriptionTimewindow.fixedWindow = { + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset, + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset + }; + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; + } + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; + subscriptionTimewindow.aggregation.interval = + timeService.boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type); + } + const aggregation = subscriptionTimewindow.aggregation; + aggregation.timeWindow = aggTimewindow; + if (aggregation.type !== AggregationType.NONE) { + aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); + } + return subscriptionTimewindow; +}; + +export const quickTimeIntervalPeriod = (interval: QuickTimeInterval): number => { switch (interval) { case QuickTimeInterval.CURRENT_HOUR: return HOUR; @@ -650,10 +664,10 @@ export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number { case QuickTimeInterval.CURRENT_YEAR_SO_FAR: return YEAR; } -} +}; -export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval, - startDate: moment_.Moment): moment_.Moment { +export const calculateIntervalComparisonStartTime = (interval: QuickTimeInterval, + startDate: moment_.Moment): moment_.Moment => { switch (interval) { case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.DAY_BEFORE_YESTERDAY: @@ -702,11 +716,11 @@ export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval startDate.subtract(1, 'hour'); return startDate.startOf('hour'); } -} +}; -export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval, +export const calculateIntervalComparisonEndTime = (interval: QuickTimeInterval, comparisonStartDate: moment_.Moment, - endDate: moment_.Moment): number { + endDate: moment_.Moment): number => { switch (interval) { case QuickTimeInterval.CURRENT_DAY_SO_FAR: return endDate.subtract(1, 'days').valueOf(); @@ -724,10 +738,10 @@ export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval, default: return calculateIntervalEndTime(interval, comparisonStartDate); } -} +}; -export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, - timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow { +export const createTimewindowForComparison = (subscriptionTimewindow: SubscriptionTimewindow, + timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow => { const timewindowForComparison: SubscriptionTimewindow = { fixedWindow: null, realtimeWindowMs: null, @@ -776,9 +790,9 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti } return timewindowForComparison; -} +}; -export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { +export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { const cloned: Timewindow = {}; cloned.hideInterval = timewindow.hideInterval || false; cloned.hideLastInterval = timewindow.hideLastInterval || false; @@ -797,7 +811,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { cloned.aggregation = deepClone(timewindow.aggregation); cloned.timezone = timewindow.timezone; return cloned; -} +}; export interface TimeInterval { name: string; @@ -937,7 +951,7 @@ export interface TimezoneInfo { let timezones: TimezoneInfo[] = null; let defaultTimezone: string = null; -export function getTimezones(): TimezoneInfo[] { +export const getTimezones = (): TimezoneInfo[] => { if (!timezones) { timezones = momentTz.tz.names().map((zoneName) => { const tz = momentTz.tz(zoneName); @@ -951,9 +965,15 @@ export function getTimezones(): TimezoneInfo[] { }); } return timezones; -} +}; -export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo { +export const getDefaultTimezone = (): string => { + if (!defaultTimezone) { + defaultTimezone = momentTz.tz.guess(); + } + return defaultTimezone; +}; +export const getTimezoneInfo = (timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo => { const timezoneList = getTimezones(); let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null; if (!foundTimezone) { @@ -965,40 +985,20 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, } } return foundTimezone; -} +}; -export function getDefaultTimezoneInfo(): TimezoneInfo { +export const getDefaultTimezoneInfo = (): TimezoneInfo => { const userTimezone = getDefaultTimezone(); return getTimezoneInfo(userTimezone); -} +}; -export function getDefaultTimezone(): string { - if (!defaultTimezone) { - defaultTimezone = momentTz.tz.guess(); - } - return defaultTimezone; -} - -export function getCurrentTime(tz?: string): moment_.Moment { - if (tz) { - return moment().tz(tz); - } else { - return moment(); - } -} - -export function getTime(ts: number, tz?: string): moment_.Moment { +export const getTime = (ts: number, tz?: string): moment_.Moment => { if (tz) { return moment(ts).tz(tz); } else { return moment(ts); } -} - -export function getTimezone(tz: string): moment_.Moment { - return moment.tz(tz); -} +}; -export function getCurrentTimeForComparison(timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment { - return getCurrentTime(tz).subtract(1, timeForComparison); -} +export const getCurrentTimeForComparison = (timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment => + getCurrentTime(tz).subtract(1, timeForComparison); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index bd7e051b58..f0ab1257d0 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -23,7 +23,13 @@ import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; import { DataKeyType } from './telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import * as moment_ from 'moment'; -import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; +import { + AlarmFilter, + AlarmFilterConfig, + EntityDataPageLink, + EntityFilter, + KeyFilter +} from '@shared/models/query/query.models'; import { PopoverPlacement } from '@shared/components/popover.models'; import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; @@ -315,14 +321,16 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', entity = 'entity', - entityCount = 'entityCount' + entityCount = 'entityCount', + alarmCount = 'alarmCount' } export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], [ DatasourceType.entity, 'entity.entity' ], - [ DatasourceType.entityCount, 'entity.entities-count' ] + [ DatasourceType.entityCount, 'entity.entities-count' ], + [ DatasourceType.alarmCount, 'entity.alarms-count' ] ] ); @@ -348,6 +356,8 @@ export interface Datasource { pageLink?: EntityDataPageLink; keyFilters?: Array; entityFilter?: EntityFilter; + alarmFilterConfig?: AlarmFilterConfig; + alarmFilter?: AlarmFilter; dataKeyStartIndex?: number; latestDataKeyStartIndex?: number; [key: string]: any; @@ -624,10 +634,7 @@ export interface WidgetConfig { actions?: {[actionSourceId: string]: Array}; settings?: WidgetSettings; alarmSource?: Datasource; - alarmStatusList?: AlarmSearchStatus[]; - alarmSeverityList?: AlarmSeverity[]; - alarmTypeList?: string[]; - searchPropagatedAlarms?: boolean; + alarmFilterConfig?: AlarmFilterConfig; datasources?: Array; targetDeviceAliasIds?: Array; [key: string]: any; 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 b236e64ceb..47d52c2069 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -429,6 +429,8 @@ "select-alarm": "Select alarm", "no-alarms-matching": "No alarms matching '{{entity}}' were found.", "alarm-required": "Alarm is required", + "alarm-filter": "Alarm filter", + "filter": "Filter", "alarm-status": "Alarm status", "alarm-status-list": "Alarm status list", "any-status": "Any status", @@ -461,6 +463,7 @@ "assignee-last-name": "Assignee last name", "assignee-email": "Assignee email", "unassigned": "Unassigned", + "assignee-not-set": "All", "status": "Status", "alarm-details": "Alarm details", "start-time": "Start time", @@ -493,7 +496,9 @@ "clear-alarm-title": "Clear Alarm", "clear-alarm-text": "Are you sure you want to clear Alarm?", "alarm-status-filter": "Alarm Status Filter", - "alarm-filter": "Alarm Filter", + "alarm-filter-title": "Alarm Filter", + "assigned": "Assigned", + "filter-title": "Filter", "max-count-load": "Maximum number of alarms to load (0 - unlimited)", "max-count-load-required": "Maximum number of alarms to load is required.", "max-count-load-error-min": "Minimum value is 0.", @@ -502,6 +507,8 @@ "fetch-size-error-min": "Minimum value is 10.", "alarm-type-list": "Alarm type list", "any-type": "Any type", + "assigned-to-current-user": "Assigned to current user", + "assigned-to-me": "Assigned to me", "search-propagated-alarms": "Search propagated alarms", "comments": "Alarm comments", "show-more": "Show more", @@ -1908,6 +1915,7 @@ "entity": "Entity", "entities": "Entities", "entities-count": "Entities count", + "alarms-count": "Alarms count", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", @@ -3735,6 +3743,7 @@ "period": "from {{ startTime }} to {{ endTime }}", "edit": "Edit timewindow", "date-range": "Date range", + "for-all-time": "For all time", "last": "Last", "time-period": "Time period", "hide": "Hide", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 70d2132bcf..e564a8c600 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -266,7 +266,7 @@ pre.tb-highlight { letter-spacing: normal; } -.tb-timewindow-panel, .tb-legend-config-panel { +.tb-timewindow-panel, .tb-legend-config-panel, .tb-alarm-filter-panel { overflow: hidden; background: #fff; border-radius: 4px;