Browse Source

UI: Alarm count datasource type support. Improve alarm filters.

feature/home-page
Igor Kulikov 4 years ago
parent
commit
14c61f65e9
  1. 137
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  2. 11
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  3. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  4. 39
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java
  5. 5
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  6. 11
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  7. 133
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  8. 80
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  9. 110
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  10. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  11. 32
      ui-ngx/src/app/core/api/alias-controller.ts
  12. 91
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  13. 4
      ui-ngx/src/app/core/api/entity-data.service.ts
  14. 14
      ui-ngx/src/app/core/http/alarm.service.ts
  15. 26
      ui-ngx/src/app/core/http/entity.service.ts
  16. 69
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  17. 8
      ui-ngx/src/app/core/services/item-buffer.service.ts
  18. 15
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  19. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  20. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  21. 203
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts
  22. 31
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html
  23. 193
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts
  24. 103
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html
  25. 29
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  26. 318
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  27. 22
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  28. 14
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html
  29. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss
  30. 10
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts
  31. 61
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  33. 2
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  34. 3
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  35. 44
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  36. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  37. 8
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  38. 12
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  39. 65
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html
  40. 119
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts
  41. 63
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  42. 40
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  43. 53
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  44. 7
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  45. 92
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  46. 1
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  47. 55
      ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw
  48. 5
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  49. 4
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  50. 10
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  51. 16
      ui-ngx/src/app/shared/components/time/timewindow.component.scss
  52. 23
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  53. 40
      ui-ngx/src/app/shared/models/alarm.models.ts
  54. 15
      ui-ngx/src/app/shared/models/query/query.models.ts
  55. 62
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  56. 368
      ui-ngx/src/app/shared/models/time/time.models.ts
  57. 21
      ui-ngx/src/app/shared/models/widget.models.ts
  58. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  59. 2
      ui-ngx/src/styles.scss

137
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.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; 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.Operation;
import org.thingsboard.server.service.security.permission.Resource; 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.UUID;
import java.util.concurrent.ExecutionException; 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. " + 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. "; "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_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_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_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_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_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_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_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<AlarmInfo> 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<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> 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<AlarmInfo> 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<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> 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)", @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). " + 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 "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH

11
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.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findCustomerAlarms(tenantId, customerId, query); return alarmService.findCustomerAlarms(tenantId, customerId, query);
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) {
return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId);

5
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.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -105,6 +106,10 @@ public interface AlarmService extends EntityDaoService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId); AlarmStatus alarmStatus, String assigneeId);

39
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<String> typeList;
private List<AlarmSearchStatus> statusList;
private List<AlarmSeverity> severityList;
private UserId assigneeId;
}

5
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.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
@ -66,6 +67,10 @@ public interface AlarmDao extends Dao<Alarm> {
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds); PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId);

11
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.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; 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.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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)); return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query));
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query));
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query));
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId) { AlarmStatus alarmStatus, String assigneeId) {

133
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -90,6 +90,57 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); 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<AlarmInfoEntity> findAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("affectedEntityId") UUID affectedEntityId,
@Param("affectedEntityType") String affectedEntityType,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> 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 " + @Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " + "FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId " + "WHERE a.tenantId = :tenantId " +
@ -124,6 +175,46 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); 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<AlarmInfoEntity> findAllAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> 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 " + @Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " + "FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " +
@ -160,6 +251,48 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); 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<AlarmInfoEntity> findCustomerAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> 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 " + @Query(value = "SELECT a.severity FROM AlarmEntity a " +
"LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " +
"WHERE a.tenantId = :tenantId " + "WHERE a.tenantId = :tenantId " +

80
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.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
@ -200,6 +201,85 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
); );
} }
@Override
public PageData<AlarmInfo> 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<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> 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<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
log.trace("Try to find customer alarms by query [{}] and pageLink [{}]", query, query.getPageLink());
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> 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 @Override
public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) { public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds);

110
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.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; 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))); 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<AlarmInfo> 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 @Test
public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { public void testFindAssignedAlarm() throws ExecutionException, InterruptedException {

5
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.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; 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.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -102,6 +103,10 @@ public interface RuleEngineAlarmService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds); PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds);

32
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 { EntityInfo } from '@shared/models/entity.models';
import { map, mergeMap } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { import {
AlarmFilter,
AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo updateDatasourceFromEntityInfo
@ -244,10 +246,14 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); 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) { if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
} }
if (newDatasource.type === DatasourceType.alarmCount) {
newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig);
}
if (newDatasource.entityAliasId) { if (newDatasource.entityAliasId) {
return this.getAliasInfo(newDatasource.entityAliasId).pipe( return this.getAliasInfo(newDatasource.entityAliasId).pipe(
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
@ -336,22 +342,30 @@ export class AliasController implements IAliasController {
map((result) => { map((result) => {
let functionIndex = 0; let functionIndex = 0;
let entityCountIndex = 0; let entityCountIndex = 0;
let alarmCountIndex = 0;
result.forEach((datasource) => { 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; let name: string;
if (datasource.name && datasource.name.length) { if (datasource.name && datasource.name.length) {
name = datasource.name; name = datasource.name;
} else { } else {
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type));
if (datasource.type === DatasourceType.function) { if (datasource.type === DatasourceType.function) {
functionIndex++; functionIndex++;
} else { if (functionIndex > 1) {
name += ' ' + functionIndex;
}
} else if (datasource.type === DatasourceType.entityCount) {
entityCountIndex++; entityCountIndex++;
} if (entityCountIndex > 1) {
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); name += ' ' + entityCountIndex;
if (datasource.type === DatasourceType.function && functionIndex > 1) { }
name += ' ' + functionIndex; } else {
} else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { alarmCountIndex++;
name += ' ' + entityCountIndex; if (alarmCountIndex > 1) {
name += ' ' + alarmCountIndex;
}
} }
} }
datasource.name = name; datasource.name = name;

91
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -23,6 +23,7 @@ import {
SubscriptionTimewindow SubscriptionTimewindow
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { import {
AlarmFilter,
ComparisonTsValue, ComparisonTsValue,
EntityData, EntityData,
EntityDataPageLink, EntityDataPageLink,
@ -36,6 +37,7 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
AggKey, AggKey,
AlarmCountCmd,
DataKeyType, DataKeyType,
EntityCountCmd, EntityCountCmd,
EntityDataCmd, EntityDataCmd,
@ -84,6 +86,7 @@ export interface EntityDataSubscriptionOptions {
dataKeys: Array<SubscriptionDataKey>; dataKeys: Array<SubscriptionDataKey>;
type: widgetType; type: widgetType;
entityFilter?: EntityFilter; entityFilter?: EntityFilter;
alarmFilter?: AlarmFilter;
isPaginatedDataSubscription?: boolean; isPaginatedDataSubscription?: boolean;
ignoreDataUpdateOnIntervalTick?: boolean; ignoreDataUpdateOnIntervalTick?: boolean;
pageLink?: EntityDataPageLink; pageLink?: EntityDataPageLink;
@ -111,6 +114,7 @@ export class EntityDataSubscription {
private dataCommand: EntityDataCmd; private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd; private subsCommand: EntityDataCmd;
private countCommand: EntityCountCmd; private countCommand: EntityCountCmd;
private alarmCountCommand: AlarmCountCmd;
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
@ -197,6 +201,7 @@ export class EntityDataSubscription {
} }
let key: string; let key: string;
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) { this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
@ -228,7 +233,8 @@ export class EntityDataSubscription {
clearTimeout(this.latestTimer); clearTimeout(this.latestTimer);
this.latestTimer = null; 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) { if (this.subscriber) {
this.subscriber.unsubscribe(); this.subscriber.unsubscribe();
this.subscriber = null; this.subscriber = null;
@ -508,6 +514,84 @@ export class EntityDataSubscription {
} }
); );
this.subscriber.subscribe(); 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<EntityData> = {
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) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null); return of(null);
@ -542,7 +626,7 @@ export class EntityDataSubscription {
this.subscriber.subscriptionCommands = [this.subsCommand]; this.subscriber.subscriptionCommands = [this.subsCommand];
this.subscriber.update(); 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.subscriber.setTsOffset(this.latestTsOffset)) {
if (this.listener.forceReInit) { if (this.listener.forceReInit) {
this.listener.forceReInit(); this.listener.forceReInit();
@ -729,7 +813,8 @@ export class EntityDataSubscription {
this.datasourceData[dataIndex] = {}; this.datasourceData[dataIndex] = {};
for (const key of Object.keys(this.dataKeys)) { for (const key of Object.keys(this.dataKeys)) {
const dataKey = this.dataKeys[key]; 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) { this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = dataKey as Array<SubscriptionDataKey>; const dataKeysList = dataKey as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeysList.length; index++) { for (let index = 0; index < dataKeysList.length; index++) {

4
ui-ngx/src/app/core/api/entity-data.service.ts

@ -175,8 +175,10 @@ export class EntityDataService {
type: subscriptionType type: subscriptionType
}; };
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.alarmCount) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.alarmFilter = datasource.alarmFilter;
entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.keyFilters = keyFilters;
entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {

14
ui-ngx/src/app/core/http/alarm.service.ts

@ -23,7 +23,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { import {
Alarm, Alarm,
AlarmInfo, AlarmInfo,
AlarmQuery, AlarmQuery, AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
AlarmSeverity, AlarmSeverity,
AlarmStatus AlarmStatus
@ -78,12 +78,24 @@ export class AlarmService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getAlarmsV2(query: AlarmQueryV2,
config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/v2/alarm${query.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getAllAlarms(query: AlarmQuery, public getAllAlarms(query: AlarmQuery,
config?: RequestConfig): Observable<PageData<AlarmInfo>> { config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/alarms${query.toQuery()}`, return this.http.get<PageData<AlarmInfo>>(`/api/alarms${query.toQuery()}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getAllAlarmsV2(query: AlarmQueryV2,
config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/v2/alarms${query.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus,
config?: RequestConfig): Observable<AlarmSeverity> { config?: RequestConfig): Observable<AlarmSeverity> {
let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`; let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`;

26
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 { AttributeService } from '@core/http/attribute.service';
import { import {
AlarmData, AlarmData,
AlarmDataQuery, AlarmDataQuery, AlarmFilter, AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
EntityData, EntityData,
EntityDataQuery, EntityDataQuery,
@ -91,6 +91,7 @@ import { AssetProfileService } from '@core/http/asset-profile.service';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { TenantProfileService } from '@core/http/tenant-profile.service'; import { TenantProfileService } from '@core/http/tenant-profile.service';
import { NotificationType } from '@shared/models/notification.models'; import { NotificationType } from '@shared/models/notification.models';
import { UserId } from '@shared/models/id/user-id';
@Injectable({ @Injectable({
providedIn: 'root' 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, public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean,
config?: RequestConfig): Observable<ImportEntitiesResultInfo> { config?: RequestConfig): Observable<ImportEntitiesResultInfo> {
const saveEntityObservable: Observable<BaseData<EntityId>> = this.getSaveEntityObservable(entityType, entityData, config); const saveEntityObservable: Observable<BaseData<EntityId>> = this.getSaveEntityObservable(entityType, entityData, config);
@ -1321,7 +1340,8 @@ export class EntityService {
dataKeys: [] dataKeys: []
}; };
this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); 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 = { datasource = {
type: subscriptionInfo.type, type: subscriptionInfo.type,
name: subscriptionInfo.name || subscriptionInfo.type, name: subscriptionInfo.name || subscriptionInfo.type,
@ -1341,7 +1361,7 @@ export class EntityService {
if (subscriptionInfo.alarmFields) { if (subscriptionInfo.alarmFields) {
this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); 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); const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count);
datasource.dataKeys.push(dataKey); datasource.dataKeys.push(dataKey);
} }

69
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -30,11 +30,12 @@ import {
WidgetLayout WidgetLayout
} from '@shared/models/dashboard.models'; } from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils'; 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 { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -201,13 +202,22 @@ export class DashboardUtilsService {
} }
public validateAndUpdateWidget(widget: Widget): Widget { public validateAndUpdateWidget(widget: Widget): Widget {
if (!widget.config) { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
widget.config = {}; // 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) { if (!widgetConfig.datasources) {
widget.config.datasources = []; widgetConfig.datasources = [];
} }
widget.config.datasources.forEach((datasource) => { widgetConfig.datasources.forEach((datasource) => {
if (datasource.type === 'device') { if (datasource.type === 'device') {
datasource.type = DatasourceType.entity; datasource.type = DatasourceType.entity;
} }
@ -216,14 +226,47 @@ export class DashboardUtilsService {
delete datasource.deviceAliasId; delete datasource.deviceAliasId;
} }
}); });
if (widget.type === widgetType.latest) { if (type === widgetType.latest) {
widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService); widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService);
} }
// Temp workaround if (type === widgetType.alarm) {
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { if (!widgetConfig.alarmFilterConfig) {
widget.typeAlias = 'basic_timeseries'; 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 { public createDefaultLayoutData(): DashboardLayout {

8
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -101,7 +101,9 @@ export class ItemBufferService {
if (datasources) { if (datasources) {
for (let i = 0; i < datasources.length; i++) { for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[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]; entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) { if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
@ -127,7 +129,9 @@ export class ItemBufferService {
if (datasources) { if (datasources) {
for (let i = 0; i < datasources.length; i++) { for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[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]; filter = dashboard.configuration.filters[datasource.filterId];
if (filter) { if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);

15
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -16,6 +16,8 @@
import { Inject, Injectable, NgZone } from '@angular/core'; import { Inject, Injectable, NgZone } from '@angular/core';
import { import {
AlarmCountCmd, AlarmCountUnsubscribeCmd,
AlarmCountUpdate,
AlarmDataCmd, AlarmDataCmd,
AlarmDataUnsubscribeCmd, AlarmDataUnsubscribeCmd,
AlarmDataUpdate, AlarmDataUpdate,
@ -26,7 +28,7 @@ import {
EntityDataCmd, EntityDataCmd,
EntityDataUnsubscribeCmd, EntityDataUnsubscribeCmd,
EntityDataUpdate, EntityDataUpdate,
GetHistoryCmd, GetHistoryCmd, isAlarmCountUpdateMsg,
isAlarmDataUpdateMsg, isAlarmDataUpdateMsg,
isEntityCountUpdateMsg, isEntityCountUpdateMsg,
isEntityDataUpdateMsg, isEntityDataUpdateMsg,
@ -80,6 +82,8 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
this.cmdWrapper.alarmDataCmds.push(subscriptionCommand); this.cmdWrapper.alarmDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityCountCmd) { } else if (subscriptionCommand instanceof EntityCountCmd) {
this.cmdWrapper.entityCountCmds.push(subscriptionCommand); this.cmdWrapper.entityCountCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
this.cmdWrapper.alarmCountCmds.push(subscriptionCommand);
} }
} }
); );
@ -123,6 +127,10 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd(); const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd); this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
const alarmCountUnsubscribeCmd = new AlarmCountUnsubscribeCmd();
alarmCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.alarmCountUnsubscribeCmds.push(alarmCountUnsubscribeCmd);
} }
const cmdId = subscriptionCommand.cmdId; const cmdId = subscriptionCommand.cmdId;
if (cmdId) { if (cmdId) {
@ -153,6 +161,11 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
if (subscriber) { if (subscriber) {
subscriber.onEntityCount(new EntityCountUpdate(message)); subscriber.onEntityCount(new EntityCountUpdate(message));
} }
} else if (isAlarmCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onAlarmCount(new AlarmCountUpdate(message));
}
} else if (message.subscriptionId) { } else if (message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId); subscriber = this.subscribersMap.get(message.subscriptionId);
if (subscriber) { if (subscriber) {

2
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -28,7 +28,7 @@
(optionSelected)="selected($event)"> (optionSelected)="selected($event)">
<mat-option [fxHide]="!assigneeId" [value]="null"> <mat-option [fxHide]="!assigneeId" [value]="null">
<mat-icon class="unassigned-icon">account_circle</mat-icon> <mat-icon class="unassigned-icon">account_circle</mat-icon>
<span translate>alarm.unassigned</span> <span>{{ assigneeNotSetText | translate }}</span>
</mat-option> </mat-option>
<mat-option *ngFor="let user of filteredUsers | async" [value]="user" <mat-option *ngFor="let user of filteredUsers | async" [value]="user"
[fxHide]="assigneeId === user.id.id"> [fxHide]="assigneeId === user.id.id">

2
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; assigneeId?: string;
assigneeNotSetText = 'alarm.unassigned';
reassigned: boolean = false; reassigned: boolean = false;
selectUserFormGroup: FormGroup; selectUserFormGroup: FormGroup;

203
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<any>('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<Array<UserEmailInfo>>;
searchText = '';
userSelected = false;
result?: UserEmailInfo;
private destroy$ = new Subject<void>();
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<Array<UserEmailInfo>> {
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<UserEmailInfo>())),
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);
}
}

31
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html

@ -0,0 +1,31 @@
<!--
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.
-->
<ng-container [formGroup]="assigneeFormGroup">
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px"
(click)="openAlarmAssigneeSelectPanel($event)"
subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label>
<input matInput readonly formControlName="assignee">
<span *ngIf="assignee" matPrefix class="user-avatar"
[style.backgroundColor]="getAvatarBgColor()">
{{ getUserInitials() }}
</span>
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon">account_circle</mat-icon>
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon>
</mat-form-field>
</ng-container>

193
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);
}
});
}
}
}

103
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html

@ -0,0 +1,103 @@
<!--
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.
-->
<ng-container *ngIf="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else alarmFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleAlarmFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #alarmFilterPanel>
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="alarmFilterConfigForm.invalid || !alarmFilterConfigForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #alarmFilter>
<div fxLayout="column" fxLayoutAlign="center" [formGroup]="alarmFilterConfigForm">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start"
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="statusList" multiple
placeholder="{{ !alarmFilterConfigForm.get('statusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="severityList" multiple
placeholder="{{ !alarmFilterConfigForm.get('severityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start"
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="typeList">
<mat-chip-row *ngFor="let type of alarmTypeList()"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input placeholder="{{ !alarmFilterConfigForm.get('typeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-grid>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutGap="16px">
<mat-slide-toggle *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="assignedToCurrentUser">
{{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }}
</mat-slide-toggle>
<tb-alarm-assignee-select
*ngIf="!alarmFilterConfigForm.get('assignedToCurrentUser').value" formControlName="assigneeId">
</tb-alarm-assignee-select>
</div>
</div>
</ng-template>

29
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss → ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss

@ -14,23 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
width: 100%; display: block;
height: 100%;
min-width: 300px;
overflow: hidden; overflow: hidden;
background: #fff; max-width: 100%;
border-radius: 4px; .mdc-button {
box-shadow: max-width: 100%;
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;
} }
}
.mat-padding { :host ::ng-deep {
padding: 16px; .mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
} }
} }

318
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<any>('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<any>;
@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(', ')}`;
}
}
}
}

22
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 { import {
AlarmInfo, AlarmInfo,
AlarmQuery, AlarmQuery,
AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
@ -45,7 +46,7 @@ import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AlarmDetailsDialogData AlarmDetailsDialogData
} from '@home/components/alarm/alarm-details-dialog.component'; } 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@ -60,21 +61,24 @@ import {
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { UtilsService } from '@core/services/utils.service'; 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<AlarmInfo, TimePageLink> { export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> {
private authUser = getCurrentAuthUser(this.store); private authUser = getCurrentAuthUser(this.store);
searchStatus: AlarmSearchStatus; alarmFilterConfig: AlarmFilterConfig;
constructor(private alarmService: AlarmService, constructor(private alarmService: AlarmService,
private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private alarmsMode: AlarmsMode = AlarmsMode.ALL, private alarmsMode: AlarmsMode = AlarmsMode.ALL,
public entityId: EntityId = null, public entityId: EntityId = null,
private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, private defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]},
private store: Store<AppState>, private store: Store<AppState>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private overlay: Overlay, private overlay: Overlay,
@ -85,8 +89,9 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
this.useTimePageLink = true; this.useTimePageLink = true;
this.forAllTimeEnabled = true;
this.pageMode = pageMode; this.pageMode = pageMode;
this.defaultTimewindowInterval = historyInterval(DAY * 30); this.defaultTimewindowInterval = forAllTimeInterval();
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = false;
this.searchEnabled = true; this.searchEnabled = true;
@ -97,7 +102,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM);
this.entityResources = { this.entityResources = {
} as EntityTypeResource<AlarmInfo>; } as EntityTypeResource<AlarmInfo>;
this.searchStatus = defaultSearchStatus; this.alarmFilterConfig = defaultAlarmFilterConfig;
this.headerComponent = AlarmTableHeaderComponent; this.headerComponent = AlarmTableHeaderComponent;
@ -159,12 +164,13 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
} }
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> { fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
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) { switch (this.alarmsMode) {
case AlarmsMode.ALL: case AlarmsMode.ALL:
return this.alarmService.getAllAlarms(query); return this.alarmService.getAllAlarmsV2(query);
case AlarmsMode.ENTITY: case AlarmsMode.ENTITY:
return this.alarmService.getAlarms(query); return this.alarmService.getAlarmsV2(query);
} }
} }

14
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html

@ -15,12 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field class="mat-block"> <tb-alarm-filter-config [ngModel]="alarmTableConfig.alarmFilterConfig"
<mat-label translate>alarm.alarm-status</mat-label> (ngModelChange)="alarmFilterChanged($event)"
<mat-select [ngModel]="alarmTableConfig.searchStatus" propagatedFilter="false"
(ngModelChange)="searchStatusChanged($event)"> userMode>
<mat-option *ngFor="let status of alarmSearchStatusTypes" [value]="status"> </tb-alarm-filter-config>
{{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>

6
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss

@ -16,9 +16,7 @@
@import '../../../../../scss/constants'; @import '../../../../../scss/constants';
:host { :host {
/* flex: 1;
display: flex;
justify-content: flex-start; */
padding-right: 8px; padding-right: 8px;
min-width: 160px; overflow: hidden;
max-width: 100%;
} }

10
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 { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models'; import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
@Component({ @Component({
selector: 'tb-alarm-table-header', selector: 'tb-alarm-table-header',
@ -28,11 +29,6 @@ import { AlarmTableConfig } from './alarm-table-config';
}) })
export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmInfo> { export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmInfo> {
alarmSearchStatusTranslationsMap = alarmSearchStatusTranslations;
alarmSearchStatusTypes = Object.keys(AlarmSearchStatus);
alarmSearchStatusEnum = AlarmSearchStatus;
get alarmTableConfig(): AlarmTableConfig { get alarmTableConfig(): AlarmTableConfig {
return this.entitiesTableConfig as AlarmTableConfig; return this.entitiesTableConfig as AlarmTableConfig;
} }
@ -41,8 +37,8 @@ export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmI
super(store); super(store);
} }
searchStatusChanged(searchStatus: AlarmSearchStatus) { alarmFilterChanged(alarmFilterConfig: AlarmFilterConfig) {
this.alarmTableConfig.searchStatus = searchStatus; this.alarmTableConfig.alarmFilterConfig = alarmFilterConfig;
this.alarmTableConfig.getTable().resetSortAndFilter(true, true); this.alarmTableConfig.getTable().resetSortAndFilter(true, true);
} }
} }

61
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts

@ -22,14 +22,24 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmSearchStatus, AlarmsMode } from '@shared/models/alarm.models'; import { AlarmSearchStatus, AlarmSeverity, AlarmsMode } from '@shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { EntityService } from '@core/http/entity.service';
import { PageQueryParam } from '@shared/models/page/page-link';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
interface AlarmPageQueryParams extends PageQueryParam {
typeList?: Array<string>;
statusList?: Array<AlarmSearchStatus>;
severityList?: Array<AlarmSeverity>;
assignedToMe?: boolean;
}
@Component({ @Component({
selector: 'tb-alarm-table', selector: 'tb-alarm-table',
@ -59,7 +69,7 @@ export class AlarmTableComponent implements OnInit {
set entityId(entityId: EntityId) { set entityId(entityId: EntityId) {
this.entityIdValue = entityId; this.entityIdValue = entityId;
if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) { if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) {
this.alarmTableConfig.searchStatus = AlarmSearchStatus.ANY; this.alarmTableConfig.alarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]};
this.alarmTableConfig.entityId = entityId; this.alarmTableConfig.entityId = entityId;
this.entitiesTable.resetSortAndFilter(this.activeValue); this.entitiesTable.resetSortAndFilter(this.activeValue);
if (!this.activeValue) { if (!this.activeValue) {
@ -73,6 +83,7 @@ export class AlarmTableComponent implements OnInit {
alarmTableConfig: AlarmTableConfig; alarmTableConfig: AlarmTableConfig;
constructor(private alarmService: AlarmService, constructor(private alarmService: AlarmService,
private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
@ -82,7 +93,8 @@ export class AlarmTableComponent implements OnInit {
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private utilsService: UtilsService, private utilsService: UtilsService,
private route: ActivatedRoute) { private route: ActivatedRoute,
private router: Router) {
} }
ngOnInit() { ngOnInit() {
@ -94,15 +106,52 @@ export class AlarmTableComponent implements OnInit {
if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) { if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) {
this.alarmsMode = 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.alarmTableConfig = new AlarmTableConfig(
this.alarmService, this.alarmService,
this.entityService,
this.dialogService, this.dialogService,
this.translate, this.translate,
this.datePipe, this.datePipe,
this.dialog, this.dialog,
this.alarmsMode, this.alarmsMode,
this.entityIdValue, this.entityIdValue,
AlarmSearchStatus.ANY, defaultAlarmFilterConfig,
this.store, this.store,
this.viewContainerRef, this.viewContainerRef,
this.overlay, this.overlay,

3
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -107,7 +107,8 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
} else { } else {
const datasources = this.utils.validateDatasources(widget.config.datasources); const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => { datasources.forEach((datasource) => {
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); this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
} }
}); });

2
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -42,7 +42,7 @@
<tb-anchor #entityTableHeader></tb-anchor> <tb-anchor #entityTableHeader></tb-anchor>
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" <tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
(ngModelChange)="onTimewindowChange()" (ngModelChange)="onTimewindowChange()"
asButton historyOnly></tb-timewindow> asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow>
</div> </div>
<span fxFlex></span> <span fxFlex></span>
<div [fxShow]="addEnabled()"> <div [fxShow]="addEnabled()">

3
ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss

@ -69,6 +69,9 @@
.mat-toolbar { .mat-toolbar {
height: auto; height: auto;
min-height: 100px; min-height: 100px;
.mat-toolbar-tools{
height: auto;
}
.tb-entity-table-title{ .tb-entity-table-title{
padding-bottom: 5px; padding-bottom: 5px;

44
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) { if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval;
const currentTime = Date.now(); const interval = this.getTimePageLinkInterval();
this.pageLink = new TimePageLink(10, 0, null, sortOrder, this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime); interval.startTime, interval.endTime);
} else { } else {
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
} }
@ -411,18 +411,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
const timePageLink = this.pageLink as TimePageLink; const timePageLink = this.pageLink as TimePageLink;
if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { const interval = this.getTimePageLinkInterval();
const currentTime = Date.now(); timePageLink.startTime = interval.startTime;
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; timePageLink.endTime = interval.endTime;
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;
}
} }
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
if (this.isDetailsOpen && this.entityDetailsPanel) { 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) { private dataLoaded(col?: number, row?: number) {
if (isFinite(col) && isFinite(row)) { if (isFinite(col) && isFinite(row)) {
this.clearCellCache(col, row); this.clearCellCache(col, row);

9
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 { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.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({ @NgModule({
declarations: declarations:
@ -202,6 +205,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
@ -352,6 +358,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
RelationFiltersComponent, RelationFiltersComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,

8
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -37,7 +37,7 @@
dndType="{{ dndId }}" dndType="{{ dndId }}"
dndEffectAllowed="move" dndEffectAllowed="move"
dndDraggingSourceClass="tb-chip-dragging" dndDraggingSourceClass="tb-chip-dragging"
[removable]="!disabled && !isEntityCountDatasource" [removable]="!disabled && !isCountDatasource"
(removed)="remove(key)"> (removed)="remove(key)">
<div dndDragImageRef fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip" <div dndDragImageRef fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip"
[ngClass]="{'tb-dragging-chip-image': $index === dragIndex}"> [ngClass]="{'tb-dragging-chip-image': $index === dragIndex}">
@ -78,7 +78,7 @@
(click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-32"> (click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-32">
<mat-icon class="tb-mat-20">edit</mat-icon> <mat-icon class="tb-mat-20">edit</mat-icon>
</button> </button>
<mat-icon matChipRemove *ngIf="!disabled && !isEntityCountDatasource">close</mat-icon> <mat-icon matChipRemove *ngIf="!disabled && !isCountDatasource">close</mat-icon>
</div> </div>
</mat-chip-row> </mat-chip-row>
<input matInput type="text" placeholder="{{ !disabled ? (keys.length ? secondaryPlaceholder : placeholder) : '' }}" <input matInput type="text" placeholder="{{ !disabled ? (keys.length ? secondaryPlaceholder : placeholder) : '' }}"
@ -87,13 +87,13 @@
#keyInput #keyInput
formControlName="key" formControlName="key"
matAutocompleteOrigin matAutocompleteOrigin
[readonly]="isEntityCountDatasource" [readonly]="isCountDatasource"
#origin="matAutocompleteOrigin" #origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin" [matAutocompleteConnectedTo]="origin"
(focusin)="onFocus()" (focusin)="onFocus()"
(drop)="$event.preventDefault();" (drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete" [matAutocomplete]="keyAutocomplete"
[matAutocompleteDisabled]="isEntityCountDatasource" [matAutocompleteDisabled]="isCountDatasource"
[matChipInputFor]="chipList" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="add($event)"> (matChipInputTokenEnd)="add($event)">

12
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; private maxDataKeysValue: number;
get maxDataKeys(): number { get maxDataKeys(): number {
return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; return this.isCountDatasource ? 1 : this.maxDataKeysValue;
} }
@Input() @Input()
@ -136,7 +136,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue || !this.optDataKeys || this.isEntityCountDatasource; return this.requiredValue || !this.optDataKeys || this.isCountDatasource;
} }
@Input() @Input()
set required(value: boolean) { set required(value: boolean) {
@ -240,7 +240,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.maxDataKeys !== null && this.maxDataKeys > -1) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); 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}); return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys});
} else { } else {
return ''; return '';
@ -287,7 +287,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private reset() { private reset() {
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys(); this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.isEntityCountDatasource) { } else if (this.isCountDatasource) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)]; this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)];
} else { } else {
this.keys = []; this.keys = [];
@ -535,8 +535,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}, 0); }, 0);
} }
get isEntityCountDatasource(): boolean { get isCountDatasource(): boolean {
return this.datasourceType === DatasourceType.entityCount; return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType);
} }
private clearSearchCache() { private clearSearchCache() {

65
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html

@ -1,65 +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.
-->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="alarmFilterFormGroup" (ngSubmit)="update()">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
placeholder="{{ !alarmFilterFormGroup.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="alarmSeverityList" multiple
placeholder="{{ !alarmFilterFormGroup.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip-row *ngFor="let type of alarmTypeList()"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input placeholder="{{ !alarmFilterFormGroup.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-grid>
</mat-form-field>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="alarmFilterFormGroup.invalid || !alarmFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

119
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts

@ -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<any>('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();
}
}

63
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -41,6 +41,7 @@ import {
deepClone, deepClone,
hashCode, hashCode,
isDefined, isDefined,
isDefinedAndNotNull,
isNumber, isNumber,
isObject, isObject,
isUndefined isUndefined
@ -73,9 +74,9 @@ import {
getColumnDefaultVisibility, getColumnDefaultVisibility,
getColumnSelectionAvailability, getColumnSelectionAvailability,
getColumnWidth, getColumnWidth,
getHeaderTitle,
getRowStyleInfo, getRowStyleInfo,
getTableCellButtonActions, getTableCellButtonActions,
getHeaderTitle,
noDataMessage, noDataMessage,
prepareTableCellButtonActions, prepareTableCellButtonActions,
RowStyleInfo, RowStyleInfo,
@ -93,8 +94,8 @@ import {
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import { import {
AlarmDataInfo, AlarmDataInfo,
alarmFields, AlarmInfo, alarmFields,
AlarmSearchStatus, AlarmInfo,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
AlarmStatus, AlarmStatus,
@ -118,23 +119,25 @@ import {
KeyFilter KeyFilter
} from '@app/shared/models/query/query.models'; } from '@app/shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.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 { entityFields } from '@shared/models/entity.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, ALARM_ASSIGNEE_PANEL_DATA,
AlarmAssigneePanelComponent,
AlarmAssigneePanelData AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component'; } from '@home/components/alarm/alarm-assignee-panel.component';
import { import {
AlarmCommentDialogComponent, AlarmCommentDialogComponent,
AlarmCommentDialogData AlarmCommentDialogData
} from '@home/components/alarm/alarm-comment-dialog.component'; } 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 { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
@ -254,6 +257,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private dialogService: DialogService, private dialogService: DialogService,
private entityService: EntityService,
private alarmService: AlarmService, private alarmService: AlarmService,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); super(store);
@ -378,17 +382,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024;
this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms) const alarmFilter = this.entityService.resolveAlarmFilter(this.widgetConfig.alarmFilterConfig);
? this.widgetConfig.searchPropagatedAlarms : true; this.pageLink = {...this.pageLink, ...alarmFilter};
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 : [];
this.noDataDisplayMessageText = this.noDataDisplayMessageText =
noDataMessage(this.widgetConfig.noDataDisplayMessage, 'alarm.no-alarms-prompt', this.utils, this.translate); 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 target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.panelClass = 'tb-alarm-filter-panel';
config.hasBackdrop = true; config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = { const connectedPosition: ConnectedPosition = {
originX: 'end', originX: 'end',
@ -612,12 +608,18 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}); });
const providers: StaticProvider[] = [ const providers: StaticProvider[] = [
{ {
provide: ALARM_FILTER_PANEL_DATA, provide: ALARM_FILTER_CONFIG_DATA,
useValue: { useValue: {
statusList: this.pageLink.statusList, panelMode: true,
severityList: this.pageLink.severityList, userMode: true,
typeList: this.pageLink.typeList alarmFilterConfig: {
} as AlarmFilterPanelData 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, provide: OverlayRef,
@ -625,14 +627,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
]; ];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); 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)); this.viewContainerRef, injector));
componentRef.onDestroy(() => { componentRef.onDestroy(() => {
if (componentRef.instance.result) { if (componentRef.instance.panelResult) {
const result = componentRef.instance.result; const result = componentRef.instance.panelResult;
this.pageLink.statusList = result.statusList; const alarmFilter = this.entityService.resolveAlarmFilter(result);
this.pageLink.severityList = result.severityList; this.pageLink = {...this.pageLink, ...alarmFilter};
this.pageLink.typeList = result.typeList;
this.resetPageIndex(); this.resetPageIndex();
this.updateData(); this.updateData();
} }

40
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 { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.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 { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-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: declarations:
[ [
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
AlarmFilterPanelComponent,
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent, AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent, TimeseriesTableWidgetComponent,
@ -75,25 +73,25 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
HomePageWidgetsModule, HomePageWidgetsModule,
SharedHomeComponentsModule SharedHomeComponentsModule
], ],
exports: [ exports: [
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent, AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent, TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent, EntitiesHierarchyWidgetComponent,
EdgesOverviewWidgetComponent, EdgesOverviewWidgetComponent,
RpcWidgetsModule, RpcWidgetsModule,
HomePageWidgetsModule, HomePageWidgetsModule,
DateRangeNavigatorWidgetComponent, DateRangeNavigatorWidgetComponent,
JsonInputWidgetComponent, JsonInputWidgetComponent,
MultipleInputWidgetComponent, MultipleInputWidgetComponent,
TripAnimationComponent, TripAnimationComponent,
PhotoCameraInputWidgetComponent, PhotoCameraInputWidgetComponent,
GatewayFormComponent, GatewayFormComponent,
NavigationCardsWidgetComponent, NavigationCardsWidgetComponent,
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent, QrCodeWidgetComponent,
MarkdownWidgetComponent MarkdownWidgetComponent
], ],
providers: [ providers: [
CustomDialogService, CustomDialogService,
ImportExportService, ImportExportService,

53
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -42,47 +42,7 @@
</section> </section>
</div> </div>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center"> <div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" <tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config>
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
placeholder="{{ !dataSettings.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="alarmSeverityList" multiple
placeholder="{{ !dataSettings.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip-row *ngFor="let type of alarmTypeList()"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input placeholder="{{ !dataSettings.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-grid>
</mat-form-field>
<mat-checkbox fxFlex formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-checkbox>
</div>
</div> </div>
<mat-accordion multi> <mat-accordion multi>
<mat-expansion-panel (opened)="extensionPanelIsOpen(true)" (closed)="extensionPanelIsOpen(false)" class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && <mat-expansion-panel (opened)="extensionPanelIsOpen(true)" (closed)="extensionPanelIsOpen(false)" class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
@ -162,8 +122,14 @@
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity || <ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''"> datasourceControl.get('type').value === datasourceType.entityCount ||
datasourceControl.get('type').value === datasourceType.alarmCount ? datasourceControl.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceControl.get('type').value === datasourceType.alarmCount"
propagatedFilter="false"
style="height: 56px; margin-bottom: 8px;"
[formControl]="datasourceControl.get('alarmFilterConfig')"></tb-alarm-filter-config>
<tb-entity-alias-select <tb-entity-alias-select
*ngIf="datasourceControl.get('type').value !== datasourceType.alarmCount"
[showLabel]="true" [showLabel]="true"
[tbRequired]="true" [tbRequired]="true"
[aliasController]="aliasController" [aliasController]="aliasController"
@ -171,12 +137,13 @@
[callbacks]="widgetConfigCallbacks"> [callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select> </tb-entity-alias-select>
<tb-filter-select <tb-filter-select
*ngIf="datasourceControl.get('type').value !== datasourceType.alarmCount"
[showLabel]="true" [showLabel]="true"
[aliasController]="aliasController" [aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')" [formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks"> [callbacks]="widgetConfigCallbacks">
</tb-filter-select> </tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount" <mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceControl.get('type').value)"
fxFlex> fxFlex>
<input matInput <input matInput
placeholder="{{ 'datasource.label' | translate }}" placeholder="{{ 'datasource.label' | translate }}"

7
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -120,6 +120,13 @@
:host ::ng-deep { :host ::ng-deep {
.tb-widget-config { .tb-widget-config {
tb-alarm-filter-config {
.mdc-button {
width: 100%;
height: 100%;
justify-content: flex-start;
}
}
.mat-mdc-tab-body-wrapper { .mat-mdc-tab-body-wrapper {
position: absolute; position: absolute;
top: 49px; top: 49px;

92
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -34,24 +34,18 @@ import {
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { import {
ControlValueAccessor, ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils'; import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils';
import { import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
alarmFields,
AlarmSearchStatus,
alarmSearchStatusTranslations,
AlarmSeverity,
alarmSeverityTranslations
} from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
@ -73,8 +67,6 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { Filter } from '@shared/models/query/query.models'; import { Filter } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
@ -105,24 +97,10 @@ const defaultSettingsForm = [
}) })
export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
widgetTypes = widgetType; widgetTypes = widgetType;
entityTypes = EntityType; entityTypes = EntityType;
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE,
AlarmSearchStatus.CLEARED,
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
@Input() @Input()
forceExpandDatasources: boolean; forceExpandDatasources: boolean;
@ -330,6 +308,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
if (this.widgetType === widgetType.latest) { if (this.widgetType === widgetType.latest) {
this.datasourceTypes.push(DatasourceType.entityCount); this.datasourceTypes.push(DatasourceType.entityCount);
this.datasourceTypes.push(DatasourceType.alarmCount);
} }
} }
@ -351,10 +330,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
}); });
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmStatusList', this.fb.control(null)); this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null));
this.dataSettings.addControl('alarmSeverityList', this.fb.control(null));
this.dataSettings.addControl('alarmTypeList', this.fb.control(null));
this.dataSettings.addControl('searchPropagatedAlarms', this.fb.control(null));
} }
} }
if (this.modelValue.isDataEnabled) { if (this.modelValue.isDataEnabled) {
@ -519,24 +495,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
targetDeviceAliasId targetDeviceAliasId
}, {emitEvent: false}); }, {emitEvent: false});
} else if (this.widgetType === widgetType.alarm) { } else if (this.widgetType === widgetType.alarm) {
let alarmStatusList: AlarmSearchStatus[] = [];
if (isDefined(config.alarmStatusList) && config.alarmStatusList.length) {
alarmStatusList = config.alarmStatusList;
} else if (isDefined(config.alarmSearchStatus) && config.alarmSearchStatus !== AlarmSearchStatus.ANY) {
alarmStatusList = [config.alarmSearchStatus];
}
this.dataSettings.patchValue(
{ alarmStatusList }, {emitEvent: false}
);
this.dataSettings.patchValue(
{ alarmSeverityList: isDefined(config.alarmSeverityList) ? config.alarmSeverityList : [] }, {emitEvent: false}
);
this.dataSettings.patchValue( this.dataSettings.patchValue(
{ alarmTypeList: isDefined(config.alarmTypeList) ? config.alarmTypeList : [] }, {emitEvent: false} { alarmFilterConfig: isDefined(config.alarmFilterConfig) ?
); config.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true } }, {emitEvent: false}
this.dataSettings.patchValue(
{ searchPropagatedAlarms: isDefined(config.searchPropagatedAlarms) ?
config.searchPropagatedAlarms : true }, {emitEvent: false}
); );
this.alarmSourceSettings.patchValue( this.alarmSourceSettings.patchValue(
config.alarmSource, {emitEvent: false} config.alarmSource, {emitEvent: false}
@ -582,7 +543,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return true; return true;
} else { } else {
return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional
&& datasource?.type !== DatasourceType.entityCount; && datasource?.type !== DatasourceType.entityCount && datasource?.type !== DatasourceType.alarmCount;
} }
} }
@ -596,7 +557,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
datasource && (datasource.type === DatasourceType.entity || datasource && (datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount) ? [Validators.required] : []], datasource.type === DatasourceType.entityCount) ? [Validators.required] : []],
filterId: [datasource ? datasource.filterId : null, []], filterId: [datasource ? datasource.filterId : null, []],
dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []],
alarmFilterConfig: [datasource && datasource.alarmFilterConfig ?
datasource.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] }]
} }
); );
if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) { if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
@ -702,37 +665,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
} }
public alarmTypeList(): string[] {
return this.dataSettings.get('alarmTypeList').value;
}
public removeAlarmType(type: string): void {
const types: string[] = this.dataSettings.get('alarmTypeList').value;
const index = types.indexOf(type);
if (index >= 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 { public displayAdvanced(): boolean {
return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema ||
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);

1
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -145,6 +145,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
loadDataOnInit = true; loadDataOnInit = true;
onLoadAction: (route: ActivatedRoute) => void = null; onLoadAction: (route: ActivatedRoute) => void = null;
useTimePageLink = false; useTimePageLink = false;
forAllTimeEnabled = false;
defaultTimewindowInterval = historyInterval(DAY); defaultTimewindowInterval = historyInterval(DAY);
entityType: EntityType = null; entityType: EntityType = null;
tableTitle = ''; tableTitle = '';

55
ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw

@ -687,18 +687,18 @@
"config": { "config": {
"datasources": [ "datasources": [
{ {
"type": "entityCount", "type": "alarmCount",
"name": null, "name": null,
"entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", "entityAliasId": null,
"filterId": null, "filterId": null,
"dataKeys": [ "dataKeys": [
{ {
"name": "count", "name": "count",
"type": "count", "type": "count",
"label": "totalAlarms", "label": "totalAlarms",
"color": "#2196f3", "color": "#ffc107",
"settings": {}, "settings": {},
"_hash": 0.8491768696709192, "_hash": 0.8743321483507485,
"aggregationType": null, "aggregationType": null,
"units": null, "units": null,
"decimals": null, "decimals": null,
@ -706,21 +706,26 @@
"usePostProcessing": null, "usePostProcessing": null,
"postFuncBody": null "postFuncBody": null
} }
] ],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
}, },
{ {
"type": "entityCount", "type": "alarmCount",
"name": null, "name": null,
"entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", "entityAliasId": null,
"filterId": "42f42812-92fe-a13a-b0ba-c6e0dfab3584", "filterId": null,
"dataKeys": [ "dataKeys": [
{ {
"name": "count", "name": "count",
"type": "count", "type": "count",
"label": "assignedToMeAlarms", "label": "assignedToMeAlarms",
"color": "#4caf50", "color": "#ffc107",
"settings": {}, "settings": {},
"_hash": 0.1262449138010293, "_hash": 0.6392390736755882,
"aggregationType": null, "aggregationType": null,
"units": null, "units": null,
"decimals": null, "decimals": null,
@ -728,21 +733,27 @@
"usePostProcessing": null, "usePostProcessing": null,
"postFuncBody": null "postFuncBody": null
} }
] ],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
],
"assignedToCurrentUser": true
}
}, },
{ {
"type": "entityCount", "type": "alarmCount",
"name": null, "name": null,
"entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", "entityAliasId": null,
"filterId": "39d92c21-2426-7346-4484-452829146c33", "filterId": null,
"dataKeys": [ "dataKeys": [
{ {
"name": "count", "name": "count",
"type": "count", "type": "count",
"label": "criticalAlarms", "label": "criticalAlarms",
"color": "#f44336", "color": "#ffc107",
"settings": {}, "settings": {},
"_hash": 0.39119172615806797, "_hash": 0.8329478098552843,
"aggregationType": null, "aggregationType": null,
"units": null, "units": null,
"decimals": null, "decimals": null,
@ -750,7 +761,15 @@
"usePostProcessing": null, "usePostProcessing": null,
"postFuncBody": null "postFuncBody": null
} }
] ],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
],
"severityList": [
"CRITICAL"
]
}
} }
], ],
"timewindow": { "timewindow": {
@ -783,7 +802,7 @@
"padding": "16px", "padding": "16px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>", "markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\" [queryParams]=\"{severityList: ['CRITICAL']}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\" [queryParams]=\"{assignedToMe: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false, "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" "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"
}, },

5
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -98,6 +98,11 @@
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval"> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;"> <div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="historyType"> <mat-radio-group formControlName="historyType">
<mat-radio-button *ngIf="forAllTimeEnabled" [value]="historyTypes.FOR_ALL_TIME" color="primary">
<section fxLayout="column">
<span translate>timewindow.for-all-time</span>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary"> <mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column"> <section fxLayout="column">
<span translate>timewindow.last</span> <span translate>timewindow.last</span>

4
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -35,6 +35,7 @@ import { OverlayRef } from '@angular/cdk/overlay';
export interface TimewindowPanelData { export interface TimewindowPanelData {
historyOnly: boolean; historyOnly: boolean;
forAllTimeEnabled: boolean;
quickIntervalOnly: boolean; quickIntervalOnly: boolean;
timewindow: Timewindow; timewindow: Timewindow;
aggregation: boolean; aggregation: boolean;
@ -53,6 +54,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyOnly = false; historyOnly = false;
forAllTimeEnabled = false;
quickIntervalOnly = false; quickIntervalOnly = false;
aggregation = false; aggregation = false;
@ -87,6 +90,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
public viewContainerRef: ViewContainerRef) { public viewContainerRef: ViewContainerRef) {
super(store); super(store);
this.historyOnly = data.historyOnly; this.historyOnly = data.historyOnly;
this.forAllTimeEnabled = data.forAllTimeEnabled;
this.quickIntervalOnly = data.quickIntervalOnly; this.quickIntervalOnly = data.quickIntervalOnly;
this.timewindow = data.timewindow; this.timewindow = data.timewindow;
this.aggregation = data.aggregation; this.aggregation = data.aggregation;

10
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<button *ngIf="asButton" <button *ngIf="asButton && !strokedButton"
[disabled]="timewindowDisabled" [disabled]="timewindowDisabled"
type="button" type="button"
mat-raised-button color="primary" mat-raised-button color="primary"
@ -23,6 +23,14 @@
<mat-icon class="material-icons">query_builder</mat-icon> <mat-icon class="material-icons">query_builder</mat-icon>
<span>{{innerValue?.displayValue}}</span> <span>{{innerValue?.displayValue}}</span>
</button> </button>
<button *ngIf="asButton && strokedButton"
[disabled]="timewindowDisabled"
type="button"
mat-stroked-button color="primary"
(click)="toggleTimewindow($event)">
<mat-icon class="material-icons">query_builder</mat-icon>
<span>{{innerValue?.displayValue}}</span>
</button>
<section *ngIf="!asButton" <section *ngIf="!asButton"
class="tb-timewindow" class="tb-timewindow"
fxLayout="row" fxLayout="row"

16
ui-ngx/src/app/shared/components/time/timewindow.component.scss

@ -16,6 +16,10 @@
:host { :host {
min-width: 52px; min-width: 52px;
margin: 8px 0; margin: 8px 0;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
section.tb-timewindow { section.tb-timewindow {
min-height: 32px; min-height: 32px;
padding: 0 6px; padding: 0 6px;
@ -33,3 +37,15 @@
} }
} }
} }
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

23
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -40,7 +40,11 @@ import {
TimewindowType TimewindowType
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TIMEWINDOW_PANEL_DATA, TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; import {
TIMEWINDOW_PANEL_DATA,
TimewindowPanelComponent,
TimewindowPanelData
} from '@shared/components/time/timewindow-panel.component';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
@ -49,6 +53,7 @@ import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
// @dynamic // @dynamic
@Component({ @Component({
@ -82,6 +87,10 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return this.historyOnlyValue; return this.historyOnlyValue;
} }
@Input()
@coerceBoolean()
forAllTimeEnabled = false;
alwaysDisplayTypePrefixValue = false; alwaysDisplayTypePrefixValue = false;
@Input() @Input()
@ -148,6 +157,10 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return this.asButtonValue; return this.asButtonValue;
} }
@Input()
@coerceBoolean()
strokedButton = false;
isEditValue = false; isEditValue = false;
@Input() @Input()
@ -222,11 +235,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
useValue: { useValue: {
timewindow: deepClone(this.innerValue), timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly, historyOnly: this.historyOnly,
forAllTimeEnabled: this.forAllTimeEnabled,
quickIntervalOnly: this.quickIntervalOnly, quickIntervalOnly: this.quickIntervalOnly,
aggregation: this.aggregation, aggregation: this.aggregation,
timezone: this.timezone, timezone: this.timezone,
isEdit: this.isEdit isEdit: this.isEdit
} } as TimewindowPanelData
}, },
{ {
provide: OverlayRef, provide: OverlayRef,
@ -296,12 +310,15 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
} }
} else { } else {
this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ? (this.translate.instant('timewindow.history') + ' - ') : ''; this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ?
(this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
} else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) { } else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) {
this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval)); this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval));
} else if (this.innerValue.history.historyType === HistoryWindowType.FOR_ALL_TIME) {
this.innerValue.displayValue += this.translate.instant('timewindow.for-all-time');
} else { } else {
const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');

40
ui-ngx/src/app/shared/models/alarm.models.ts

@ -25,6 +25,7 @@ import { CustomerId } from '@shared/models/id/customer-id';
import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models';
import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; import { AlarmCommentId } from '@shared/models/id/alarm-comment-id';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
import { AlarmFilter } from '@shared/models/query/query.models';
export enum AlarmsMode { export enum AlarmsMode {
ALL, ALL,
@ -300,3 +301,42 @@ export class AlarmQuery {
} }
} }
export class AlarmQueryV2 {
affectedEntityId: EntityId;
pageLink: TimePageLink;
typeList: string[];
statusList: AlarmSearchStatus[];
severityList: AlarmSeverity[];
assigneeId?: UserId;
constructor(entityId: EntityId, pageLink: TimePageLink,
alarmFilter: AlarmFilter) {
this.affectedEntityId = entityId;
this.pageLink = pageLink;
this.typeList = alarmFilter.typeList;
this.statusList = alarmFilter.statusList;
this.severityList = alarmFilter.severityList;
this.assigneeId = alarmFilter.assigneeId;
}
public toQuery(): string {
let query = this.affectedEntityId ? `/${this.affectedEntityId.entityType}/${this.affectedEntityId.id}` : '';
query += this.pageLink.toQuery();
if (this.typeList && this.typeList.length) {
query += `&typeList=${this.typeList.join(',')}`;
}
if (this.statusList && this.statusList.length) {
query += `&statusList=${this.statusList.join(',')}`;
}
if (this.severityList && this.severityList.length) {
query += `&severityList=${this.severityList.join(',')}`;
}
if (typeof this.assigneeId !== 'undefined' && this.assigneeId !== null) {
query += `&assigneeId=${this.assigneeId.id}`;
}
return query;
}
}

15
ui-ngx/src/app/shared/models/query/query.models.ts

@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models';
import { Filter } from '@material-ui/icons'; import { Filter } from '@material-ui/icons';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UserId } from '../id/user-id';
export enum EntityKeyType { export enum EntityKeyType {
ATTRIBUTE = 'ATTRIBUTE', ATTRIBUTE = 'ATTRIBUTE',
@ -705,7 +706,7 @@ export interface EntityDataPageLink {
dynamic?: boolean; dynamic?: boolean;
} }
export interface AlarmDataPageLink extends EntityDataPageLink { export interface AlarmFilter {
startTs?: number; startTs?: number;
endTs?: number; endTs?: number;
timeWindow?: number; timeWindow?: number;
@ -713,8 +714,17 @@ export interface AlarmDataPageLink extends EntityDataPageLink {
statusList?: Array<AlarmSearchStatus>; statusList?: Array<AlarmSearchStatus>;
severityList?: Array<AlarmSeverity>; severityList?: Array<AlarmSeverity>;
searchPropagatedAlarms?: boolean; 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 { export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection {
if (pageLink.sortOrder) { if (pageLink.sortOrder) {
return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection; return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection;
@ -843,7 +853,8 @@ export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: E
}; };
datasource.entityId = entity.id; datasource.entityId = entity.id;
datasource.entityType = entity.entityType; 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) { if (datasource.type === DatasourceType.entity) {
datasource.entityName = entity.name; datasource.entityName = entity.name;
datasource.entityLabel = entity.label; datasource.entityLabel = entity.label;

62
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 { map } from 'rxjs/operators';
import { NgZone } from '@angular/core'; import { NgZone } from '@angular/core';
import { import {
AlarmCountQuery,
AlarmData, AlarmData,
AlarmDataQuery, EntityCountQuery, AlarmDataQuery, EntityCountQuery,
EntityData, EntityData,
@ -224,6 +225,11 @@ export class AlarmDataCmd implements WebsocketCmd {
} }
} }
export class AlarmCountCmd implements WebsocketCmd {
cmdId: number;
query?: AlarmCountQuery;
}
export class EntityDataUnsubscribeCmd implements WebsocketCmd { export class EntityDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
} }
@ -236,6 +242,10 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
} }
export class AlarmCountUnsubscribeCmd implements WebsocketCmd {
cmdId: number;
}
export class TelemetryPluginCmdsWrapper implements CmdWrapper { export class TelemetryPluginCmdsWrapper implements CmdWrapper {
constructor() { constructor() {
@ -248,6 +258,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
this.alarmDataUnsubscribeCmds = []; this.alarmDataUnsubscribeCmds = [];
this.entityCountCmds = []; this.entityCountCmds = [];
this.entityCountUnsubscribeCmds = []; this.entityCountUnsubscribeCmds = [];
this.alarmCountCmds = [];
this.alarmCountUnsubscribeCmds = [];
} }
attrSubCmds: Array<AttributesSubscriptionCmd>; attrSubCmds: Array<AttributesSubscriptionCmd>;
tsSubCmds: Array<TimeseriesSubscriptionCmd>; tsSubCmds: Array<TimeseriesSubscriptionCmd>;
@ -258,6 +270,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>; alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>;
entityCountCmds: Array<EntityCountCmd>; entityCountCmds: Array<EntityCountCmd>;
entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>; entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>;
alarmCountCmds: Array<AlarmCountCmd>;
alarmCountUnsubscribeCmds: Array<AlarmCountUnsubscribeCmd>;
private static popCmds<T>(cmds: Array<T>, leftCount: number): Array<T> { private static popCmds<T>(cmds: Array<T>, leftCount: number): Array<T> {
const toPublish = Math.min(cmds.length, leftCount); const toPublish = Math.min(cmds.length, leftCount);
@ -277,7 +291,9 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
this.alarmDataCmds.length > 0 || this.alarmDataCmds.length > 0 ||
this.alarmDataUnsubscribeCmds.length > 0 || this.alarmDataUnsubscribeCmds.length > 0 ||
this.entityCountCmds.length > 0 || this.entityCountCmds.length > 0 ||
this.entityCountUnsubscribeCmds.length > 0; this.entityCountUnsubscribeCmds.length > 0 ||
this.alarmCountCmds.length > 0 ||
this.alarmCountUnsubscribeCmds.length > 0;
} }
public clear() { public clear() {
@ -290,6 +306,8 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
this.alarmDataUnsubscribeCmds.length = 0; this.alarmDataUnsubscribeCmds.length = 0;
this.entityCountCmds.length = 0; this.entityCountCmds.length = 0;
this.entityCountUnsubscribeCmds.length = 0; this.entityCountUnsubscribeCmds.length = 0;
this.alarmCountCmds.length = 0;
this.alarmCountUnsubscribeCmds.length = 0;
} }
public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
@ -312,6 +330,10 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount); preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount);
leftCount -= preparedWrapper.entityCountCmds.length; leftCount -= preparedWrapper.entityCountCmds.length;
preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount); 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; return preparedWrapper;
} }
} }
@ -337,6 +359,7 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder {
export enum CmdUpdateType { export enum CmdUpdateType {
ENTITY_DATA = 'ENTITY_DATA', ENTITY_DATA = 'ENTITY_DATA',
ALARM_DATA = 'ALARM_DATA', ALARM_DATA = 'ALARM_DATA',
ALARM_COUNT_DATA = 'ALARM_COUNT_DATA',
COUNT_DATA = 'COUNT_DATA', COUNT_DATA = 'COUNT_DATA',
NOTIFICATIONS_COUNT = 'NOTIFICATIONS_COUNT', NOTIFICATIONS_COUNT = 'NOTIFICATIONS_COUNT',
NOTIFICATIONS = 'NOTIFICATIONS' NOTIFICATIONS = 'NOTIFICATIONS'
@ -369,7 +392,13 @@ export interface EntityCountUpdateMsg extends CmdUpdateMsg {
count: number; 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 => { export const isEntityDataUpdateMsg = (message: WebsocketDataMsg): message is EntityDataUpdateMsg => {
const updateMsg = (message as CmdUpdateMsg); 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; 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 { export class SubscriptionUpdate implements SubscriptionUpdateMsg {
subscriptionId: number; subscriptionId: number;
errorCode: 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 { export class TelemetrySubscriber extends WsSubscriber {
private dataSubject = new ReplaySubject<SubscriptionUpdate>(1); private dataSubject = new ReplaySubject<SubscriptionUpdate>(1);
private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1); private entityDataSubject = new ReplaySubject<EntityDataUpdate>(1);
private alarmDataSubject = new ReplaySubject<AlarmDataUpdate>(1); private alarmDataSubject = new ReplaySubject<AlarmDataUpdate>(1);
private entityCountSubject = new ReplaySubject<EntityCountUpdate>(1); private entityCountSubject = new ReplaySubject<EntityCountUpdate>(1);
private alarmCountSubject = new ReplaySubject<AlarmCountUpdate>(1);
private tsOffset = undefined; private tsOffset = undefined;
public data$ = this.dataSubject.asObservable(); public data$ = this.dataSubject.asObservable();
public entityData$ = this.entityDataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable();
public alarmData$ = this.alarmDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable();
public entityCount$ = this.entityCountSubject.asObservable(); public entityCount$ = this.entityCountSubject.asObservable();
public alarmCount$ = this.alarmCountSubject.asObservable();
public static createEntityAttributesSubscription(telemetryService: TelemetryWebsocketService, public static createEntityAttributesSubscription(telemetryService: TelemetryWebsocketService,
entityId: EntityId, attributeScope: TelemetryType, entityId: EntityId, attributeScope: TelemetryType,
@ -629,6 +674,7 @@ export class TelemetrySubscriber extends WsSubscriber {
this.entityDataSubject.complete(); this.entityDataSubject.complete();
this.alarmDataSubject.complete(); this.alarmDataSubject.complete();
this.entityCountSubject.complete(); this.entityCountSubject.complete();
this.alarmCountSubject.complete();
super.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<Array<AttributeData>> { public attributeData$(): Observable<Array<AttributeData>> {
const attributeData = new Array<AttributeData>(); const attributeData = new Array<AttributeData>();
return this.data$.pipe( return this.data$.pipe(

368
ui-ngx/src/app/shared/models/time/time.models.ts

@ -43,7 +43,8 @@ export enum RealtimeWindowType {
export enum HistoryWindowType { export enum HistoryWindowType {
LAST_INTERVAL, LAST_INTERVAL,
FIXED, FIXED,
INTERVAL INTERVAL,
FOR_ALL_TIME
} }
export interface IntervalWindow { export interface IntervalWindow {
@ -187,17 +188,22 @@ export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string
[QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far'] [QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
]); ]);
export function historyInterval(timewindowMs: number): Timewindow { export const forAllTimeInterval = (): Timewindow => ({
return { selectedTab: TimewindowType.HISTORY,
selectedTab: TimewindowType.HISTORY, history: {
history: { historyType: HistoryWindowType.FOR_ALL_TIME
historyType: HistoryWindowType.LAST_INTERVAL, }
timewindowMs });
}
};
}
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(); const currentTime = moment().valueOf();
return { return {
displayValue: '', displayValue: '',
@ -229,17 +235,17 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
limit: Math.floor(timeService.getMaxDatapointsLimit() / 2) limit: Math.floor(timeService.getMaxDatapointsLimit() / 2)
} }
}; };
} };
function getTimewindowType(timewindow: Timewindow): TimewindowType { const getTimewindowType = (timewindow: Timewindow): TimewindowType => {
if (isUndefined(timewindow.selectedTab)) { if (isUndefined(timewindow.selectedTab)) {
return isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; return isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
} else { } else {
return timewindow.selectedTab; 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); const model = defaultTimewindow(timeService);
if (value) { if (value) {
model.hideInterval = value.hideInterval; model.hideInterval = value.hideInterval;
@ -311,10 +317,10 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO
model.realtime.realtimeType = RealtimeWindowType.INTERVAL; model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
} }
return model; return model;
} };
export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, endTimeMs: number, export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number,
interval: number, timeService: TimeService): Timewindow { interval: number, timeService: TimeService): Timewindow => {
if (timewindow.history) { if (timewindow.history) {
interval = isDefined(interval) ? interval : timewindow.history.interval; interval = isDefined(interval) ? interval : timewindow.history.interval;
} else if (timewindow.realtime) { } else if (timewindow.realtime) {
@ -353,18 +359,20 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
}, },
timezone: timewindow.timezone timezone: timewindow.timezone
}; };
} };
export function timewindowTypeChanged(newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean { export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean => {
if (!newTimewindow || !oldTimewindow) { if (!newTimewindow || !oldTimewindow) {
return false; return false;
} }
const newType = getTimewindowType(newTimewindow); const newType = getTimewindowType(newTimewindow);
const oldType = getTimewindowType(oldTimewindow); const oldType = getTimewindowType(oldTimewindow);
return newType !== oldType; 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) { if (timezone) {
const tz = getTimezone(timezone); const tz = getTimezone(timezone);
const localOffset = moment().utcOffset(); const localOffset = moment().utcOffset();
@ -372,115 +380,19 @@ export function calculateTsOffset(timezone?: string): number {
} else { } else {
return 0; return 0;
} }
} };
export function isHistoryTypeTimewindow(timewindow: Timewindow): boolean { export const isHistoryTypeTimewindow = (timewindow: Timewindow): boolean => getTimewindowType(timewindow) === TimewindowType.HISTORY;
return getTimewindowType(timewindow) === TimewindowType.HISTORY;
}
export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, export const getCurrentTime = (tz?: string): moment_.Moment => {
timeService: TimeService): SubscriptionTimewindow { if (tz) {
const subscriptionTimewindow: SubscriptionTimewindow = { return moment().tz(tz);
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 { } else {
let historyType = timewindow.history.historyType; return moment();
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; };
}
function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, tz?: string): number { const getSubscriptionRealtimeWindowFromTimeInterval = (interval: QuickTimeInterval, tz?: string): number => {
let currentDate; let currentDate;
switch (interval) { switch (interval) {
case QuickTimeInterval.CURRENT_HOUR: case QuickTimeInterval.CURRENT_HOUR:
@ -514,19 +426,9 @@ function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterv
currentDate = getCurrentTime(tz); currentDate = getCurrentTime(tz);
return currentDate.endOf('year').diff(currentDate.clone().startOf('year')); 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) { switch (interval) {
case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days'); currentDate.subtract(1, 'days');
@ -577,9 +479,9 @@ export function calculateIntervalStartTime(interval: QuickTimeInterval, currentD
case QuickTimeInterval.CURRENT_YEAR_SO_FAR: case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.startOf('year'); 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) { switch (interval) {
case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY: case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
@ -614,9 +516,121 @@ export function calculateIntervalEndTime(interval: QuickTimeInterval, startDate:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR: case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return getCurrentTime(tz).valueOf(); 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) { switch (interval) {
case QuickTimeInterval.CURRENT_HOUR: case QuickTimeInterval.CURRENT_HOUR:
return HOUR; return HOUR;
@ -650,10 +664,10 @@ export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
case QuickTimeInterval.CURRENT_YEAR_SO_FAR: case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return YEAR; return YEAR;
} }
} };
export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval, export const calculateIntervalComparisonStartTime = (interval: QuickTimeInterval,
startDate: moment_.Moment): moment_.Moment { startDate: moment_.Moment): moment_.Moment => {
switch (interval) { switch (interval) {
case QuickTimeInterval.YESTERDAY: case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY: case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
@ -702,11 +716,11 @@ export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval
startDate.subtract(1, 'hour'); startDate.subtract(1, 'hour');
return startDate.startOf('hour'); return startDate.startOf('hour');
} }
} };
export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval, export const calculateIntervalComparisonEndTime = (interval: QuickTimeInterval,
comparisonStartDate: moment_.Moment, comparisonStartDate: moment_.Moment,
endDate: moment_.Moment): number { endDate: moment_.Moment): number => {
switch (interval) { switch (interval) {
case QuickTimeInterval.CURRENT_DAY_SO_FAR: case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return endDate.subtract(1, 'days').valueOf(); return endDate.subtract(1, 'days').valueOf();
@ -724,10 +738,10 @@ export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval,
default: default:
return calculateIntervalEndTime(interval, comparisonStartDate); return calculateIntervalEndTime(interval, comparisonStartDate);
} }
} };
export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, export const createTimewindowForComparison = (subscriptionTimewindow: SubscriptionTimewindow,
timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow { timeUnit: ComparisonDuration, customIntervalValue: number): SubscriptionTimewindow => {
const timewindowForComparison: SubscriptionTimewindow = { const timewindowForComparison: SubscriptionTimewindow = {
fixedWindow: null, fixedWindow: null,
realtimeWindowMs: null, realtimeWindowMs: null,
@ -776,9 +790,9 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti
} }
return timewindowForComparison; return timewindowForComparison;
} };
export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
const cloned: Timewindow = {}; const cloned: Timewindow = {};
cloned.hideInterval = timewindow.hideInterval || false; cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideLastInterval = timewindow.hideLastInterval || false; cloned.hideLastInterval = timewindow.hideLastInterval || false;
@ -797,7 +811,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
cloned.aggregation = deepClone(timewindow.aggregation); cloned.aggregation = deepClone(timewindow.aggregation);
cloned.timezone = timewindow.timezone; cloned.timezone = timewindow.timezone;
return cloned; return cloned;
} };
export interface TimeInterval { export interface TimeInterval {
name: string; name: string;
@ -937,7 +951,7 @@ export interface TimezoneInfo {
let timezones: TimezoneInfo[] = null; let timezones: TimezoneInfo[] = null;
let defaultTimezone: string = null; let defaultTimezone: string = null;
export function getTimezones(): TimezoneInfo[] { export const getTimezones = (): TimezoneInfo[] => {
if (!timezones) { if (!timezones) {
timezones = momentTz.tz.names().map((zoneName) => { timezones = momentTz.tz.names().map((zoneName) => {
const tz = momentTz.tz(zoneName); const tz = momentTz.tz(zoneName);
@ -951,9 +965,15 @@ export function getTimezones(): TimezoneInfo[] {
}); });
} }
return timezones; 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(); const timezoneList = getTimezones();
let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null; let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null;
if (!foundTimezone) { if (!foundTimezone) {
@ -965,40 +985,20 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string,
} }
} }
return foundTimezone; return foundTimezone;
} };
export function getDefaultTimezoneInfo(): TimezoneInfo { export const getDefaultTimezoneInfo = (): TimezoneInfo => {
const userTimezone = getDefaultTimezone(); const userTimezone = getDefaultTimezone();
return getTimezoneInfo(userTimezone); return getTimezoneInfo(userTimezone);
} };
export function getDefaultTimezone(): string { export const getTime = (ts: number, tz?: string): moment_.Moment => {
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 {
if (tz) { if (tz) {
return moment(ts).tz(tz); return moment(ts).tz(tz);
} else { } else {
return moment(ts); 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 { export const getCurrentTimeForComparison = (timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment =>
return getCurrentTime(tz).subtract(1, timeForComparison); getCurrentTime(tz).subtract(1, timeForComparison);
}

21
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 { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment'; 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 { PopoverPlacement } from '@shared/components/popover.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core';
@ -315,14 +321,16 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType { export enum DatasourceType {
function = 'function', function = 'function',
entity = 'entity', entity = 'entity',
entityCount = 'entityCount' entityCount = 'entityCount',
alarmCount = 'alarmCount'
} }
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>( export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[ [
[ DatasourceType.function, 'function.function' ], [ DatasourceType.function, 'function.function' ],
[ DatasourceType.entity, 'entity.entity' ], [ 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; pageLink?: EntityDataPageLink;
keyFilters?: Array<KeyFilter>; keyFilters?: Array<KeyFilter>;
entityFilter?: EntityFilter; entityFilter?: EntityFilter;
alarmFilterConfig?: AlarmFilterConfig;
alarmFilter?: AlarmFilter;
dataKeyStartIndex?: number; dataKeyStartIndex?: number;
latestDataKeyStartIndex?: number; latestDataKeyStartIndex?: number;
[key: string]: any; [key: string]: any;
@ -624,10 +634,7 @@ export interface WidgetConfig {
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: WidgetSettings; settings?: WidgetSettings;
alarmSource?: Datasource; alarmSource?: Datasource;
alarmStatusList?: AlarmSearchStatus[]; alarmFilterConfig?: AlarmFilterConfig;
alarmSeverityList?: AlarmSeverity[];
alarmTypeList?: string[];
searchPropagatedAlarms?: boolean;
datasources?: Array<Datasource>; datasources?: Array<Datasource>;
targetDeviceAliasIds?: Array<string>; targetDeviceAliasIds?: Array<string>;
[key: string]: any; [key: string]: any;

11
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -429,6 +429,8 @@
"select-alarm": "Select alarm", "select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.", "no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required", "alarm-required": "Alarm is required",
"alarm-filter": "Alarm filter",
"filter": "Filter",
"alarm-status": "Alarm status", "alarm-status": "Alarm status",
"alarm-status-list": "Alarm status list", "alarm-status-list": "Alarm status list",
"any-status": "Any status", "any-status": "Any status",
@ -461,6 +463,7 @@
"assignee-last-name": "Assignee last name", "assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email", "assignee-email": "Assignee email",
"unassigned": "Unassigned", "unassigned": "Unassigned",
"assignee-not-set": "All",
"status": "Status", "status": "Status",
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
"start-time": "Start time", "start-time": "Start time",
@ -493,7 +496,9 @@
"clear-alarm-title": "Clear Alarm", "clear-alarm-title": "Clear Alarm",
"clear-alarm-text": "Are you sure you want to clear Alarm?", "clear-alarm-text": "Are you sure you want to clear Alarm?",
"alarm-status-filter": "Alarm Status Filter", "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": "Maximum number of alarms to load (0 - unlimited)",
"max-count-load-required": "Maximum number of alarms to load is required.", "max-count-load-required": "Maximum number of alarms to load is required.",
"max-count-load-error-min": "Minimum value is 0.", "max-count-load-error-min": "Minimum value is 0.",
@ -502,6 +507,8 @@
"fetch-size-error-min": "Minimum value is 10.", "fetch-size-error-min": "Minimum value is 10.",
"alarm-type-list": "Alarm type list", "alarm-type-list": "Alarm type list",
"any-type": "Any type", "any-type": "Any type",
"assigned-to-current-user": "Assigned to current user",
"assigned-to-me": "Assigned to me",
"search-propagated-alarms": "Search propagated alarms", "search-propagated-alarms": "Search propagated alarms",
"comments": "Alarm comments", "comments": "Alarm comments",
"show-more": "Show more", "show-more": "Show more",
@ -1908,6 +1915,7 @@
"entity": "Entity", "entity": "Entity",
"entities": "Entities", "entities": "Entities",
"entities-count": "Entities count", "entities-count": "Entities count",
"alarms-count": "Alarms count",
"aliases": "Entity aliases", "aliases": "Entity aliases",
"entity-alias": "Entity alias", "entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias",
@ -3735,6 +3743,7 @@
"period": "from {{ startTime }} to {{ endTime }}", "period": "from {{ startTime }} to {{ endTime }}",
"edit": "Edit timewindow", "edit": "Edit timewindow",
"date-range": "Date range", "date-range": "Date range",
"for-all-time": "For all time",
"last": "Last", "last": "Last",
"time-period": "Time period", "time-period": "Time period",
"hide": "Hide", "hide": "Hide",

2
ui-ngx/src/styles.scss

@ -266,7 +266,7 @@ pre.tb-highlight {
letter-spacing: normal; letter-spacing: normal;
} }
.tb-timewindow-panel, .tb-legend-config-panel { .tb-timewindow-panel, .tb-legend-config-panel, .tb-alarm-filter-panel {
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;

Loading…
Cancel
Save