Browse Source

Merge branch 'develop/3.5' of https://github.com/thingsboard/thingsboard into assignee

pull/8370/head
Seraphym-Tuhai 3 years ago
parent
commit
e1413e3cd0
  1. 71
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 137
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 11
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  4. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  5. 39
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java
  6. 5
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  7. 11
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  8. 133
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  9. 80
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  10. 110
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  11. 2
      pull_request_template.md
  12. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  13. 6
      ui-ngx/angular.json
  14. 32
      ui-ngx/src/app/core/api/alias-controller.ts
  15. 91
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  16. 4
      ui-ngx/src/app/core/api/entity-data.service.ts
  17. 14
      ui-ngx/src/app/core/http/alarm.service.ts
  18. 26
      ui-ngx/src/app/core/http/entity.service.ts
  19. 5
      ui-ngx/src/app/core/http/notification.service.ts
  20. 1
      ui-ngx/src/app/core/http/public-api.ts
  21. 37
      ui-ngx/src/app/core/http/usage-info.service.ts
  22. 23
      ui-ngx/src/app/core/http/user-settings.service.ts
  23. 69
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  24. 8
      ui-ngx/src/app/core/services/item-buffer.service.ts
  25. 1
      ui-ngx/src/app/core/services/menu.models.ts
  26. 193
      ui-ngx/src/app/core/services/menu.service.ts
  27. 15
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  28. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  29. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  30. 203
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts
  31. 31
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html
  32. 193
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts
  33. 103
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html
  34. 29
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  35. 318
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  36. 22
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  37. 14
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html
  38. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss
  39. 10
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts
  40. 61
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  41. 7
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  42. 2
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  43. 3
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  44. 44
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  45. 8
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  46. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  47. 3
      ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts
  48. 8
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  49. 12
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  50. 65
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html
  51. 119
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts
  52. 63
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  53. 1
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  54. 2
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  55. 33
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html
  56. 29
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.scss
  57. 57
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts
  58. 14
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss
  59. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts
  60. 10
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html
  61. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts
  62. 16
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html
  63. 27
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts
  64. 63
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.html
  65. 114
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.ts
  66. 95
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html
  67. 4
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.scss
  68. 122
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts
  69. 123
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html
  70. 117
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss
  71. 4
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts
  72. 70
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  73. 30
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
  74. 33
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss
  75. 10
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss
  76. 52
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss
  77. 91
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html
  78. 318
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts
  79. 51
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
  80. 168
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts
  81. 106
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  82. 240
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss
  83. 222
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
  84. 28
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html
  85. 134
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss
  86. 90
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts
  87. 89
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  88. 113
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss
  89. 111
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
  90. 18
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss
  91. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts
  92. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  93. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
  94. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html
  95. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts
  96. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  97. 40
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  98. 53
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  99. 7
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  100. 92
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

71
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

File diff suppressed because one or more lines are too long

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 {

2
pull_request_template.md

@ -5,7 +5,7 @@ Put your PR description here instead of this sentence.
## General checklist ## General checklist
- [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing). - [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing).
- [ ] PR name contains fix version. For example, "[3.3.4] Hotfix of some UI component" or "[3.4] New Super Feature". - [ ] [Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels#about-labels) that classify your pull request have been added.
- [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version. - [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version.
- [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues). - [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues).
- [ ] Description contains human-readable scope of changes. - [ ] Description contains human-readable scope of changes.

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

6
ui-ngx/angular.json

@ -54,6 +54,11 @@
"input": "./node_modules/ace-builds/src-noconflict/", "input": "./node_modules/ace-builds/src-noconflict/",
"output": "/" "output": "/"
}, },
{
"glob": "worker-tbel.js",
"input": "./src/app/shared/models/ace/tbel/",
"output": "/"
},
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/tinymce", "input": "node_modules/tinymce",
@ -85,6 +90,7 @@
"node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css", "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css",
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"src/app/modules/home/components/widget/lib/maps/markers.scss", "src/app/modules/home/components/widget/lib/maps/markers.scss",
"src/app/modules/home/components/widget/lib/home-page/home-page.scss",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css", "node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css",

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, false);
}
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, searchPropagatedByDefault = true): 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 : searchPropagatedByDefault;
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);
} }

5
ui-ngx/src/app/core/http/notification.service.ts

@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { import {
Notification, Notification,
NotificationDeliveryMethod,
NotificationRequest, NotificationRequest,
NotificationRequestInfo, NotificationRequestInfo,
NotificationRequestPreview, NotificationRequestPreview,
@ -71,6 +72,10 @@ export class NotificationService {
return this.http.get<NotificationRequest>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); return this.http.get<NotificationRequest>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
} }
public getAvailableDeliveryMethods(config?: RequestConfig): Observable<Array<NotificationDeliveryMethod>> {
return this.http.get<Array<NotificationDeliveryMethod>>(`/api/notification/deliveryMethods`, defaultHttpOptionsFromConfig(config));
}
public deleteNotificationRequest(id: string, config?: RequestConfig): Observable<void> { public deleteNotificationRequest(id: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
} }

1
ui-ngx/src/app/core/http/public-api.ts

@ -42,3 +42,4 @@ export * from './ui-settings.service';
export * from './user.service'; export * from './user.service';
export * from './user-settings.service'; export * from './user-settings.service';
export * from './widget.service'; export * from './widget.service';
export * from './usage-info.service';

37
ui-ngx/src/app/core/http/usage-info.service.ts

@ -0,0 +1,37 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { UsageInfo } from '@shared/models/usage.models';
@Injectable({
providedIn: 'root'
})
export class UsageInfoService {
constructor(
private http: HttpClient
) {}
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> {
return this.http.get<UsageInfo>('/api/usage',
defaultHttpOptionsFromConfig(config));
}
}

23
ui-ngx/src/app/core/http/user-settings.service.ts

@ -19,7 +19,7 @@ import { Observable } from 'rxjs';
import { import {
DocumentationLink, DocumentationLinks, DocumentationLink, DocumentationLinks,
GettingStarted, GettingStarted,
initialUserSettings, initialUserSettings, QuickLinks, UserDashboardAction, UserDashboardsInfo,
UserSettings, UserSettings,
UserSettingsType UserSettingsType
} from '@shared/models/user-settings.models'; } from '@shared/models/user-settings.models';
@ -67,6 +67,16 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getQuickLinks(config?: RequestConfig): Observable<QuickLinks> {
return this.http.get<QuickLinks>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`,
defaultHttpOptionsFromConfig(config));
}
public updateQuickLinks(quickLinks: QuickLinks, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`, quickLinks,
defaultHttpOptionsFromConfig(config));
}
public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> { public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> {
return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
@ -77,4 +87,15 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getUserDashboardsInfo(config?: RequestConfig): Observable<UserDashboardsInfo> {
return this.http.get<UserDashboardsInfo>('/api/user/dashboards',
defaultHttpOptionsFromConfig(config));
}
public reportUserDashboardAction(dashboardId: string, action: UserDashboardAction,
config?: RequestConfig): Observable<UserDashboardsInfo> {
return this.http.get<UserDashboardsInfo>(`/api/user/dashboards/${dashboardId}/${action}`,
defaultHttpOptionsFromConfig(config));
}
} }

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

1
ui-ngx/src/app/core/services/menu.models.ts

@ -20,6 +20,7 @@ export declare type MenuSectionType = 'link' | 'toggle';
export interface MenuSection extends HasUUID{ export interface MenuSection extends HasUUID{
name: string; name: string;
fullName?: string;
type: MenuSectionType; type: MenuSectionType;
path: string; path: string;
icon: string; icon: string;

193
ui-ngx/src/app/core/services/menu.service.ts

@ -18,11 +18,10 @@ import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { filter, take } from 'rxjs/operators'; import { filter, map, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models'; import { HomeSection, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { guid } from '@core/utils';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
@ -34,6 +33,9 @@ export class MenuService {
currentMenuSections: Array<MenuSection>; currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]); menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]); homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items))
);
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private router: Router) { private router: Router) {
@ -91,21 +93,21 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'tenants',
name: 'tenant.tenants', name: 'tenant.tenants',
type: 'link', type: 'link',
path: '/tenants', path: '/tenants',
icon: 'supervisor_account' icon: 'supervisor_account'
}, },
{ {
id: guid(), id: 'tenant_profiles',
name: 'tenant-profile.tenant-profiles', name: 'tenant-profile.tenant-profiles',
type: 'link', type: 'link',
path: '/tenantProfiles', path: '/tenantProfiles',
@ -113,21 +115,21 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'resources',
name: 'admin.resources', name: 'admin.resources',
type: 'toggle', type: 'toggle',
path: '/resources', path: '/resources',
icon: 'folder', icon: 'folder',
pages: [ pages: [
{ {
id: guid(), id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-bundles',
icon: 'now_widgets' icon: 'now_widgets'
}, },
{ {
id: guid(), id: 'resources_library',
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
@ -137,7 +139,7 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -145,37 +147,42 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
}, },
{ {
id: guid(), id: 'notification_sent',
name: 'notification.sent', name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link', type: 'link',
path: '/notification/sent', path: '/notification/sent',
icon: 'outbox' icon: 'outbox'
}, },
{ {
id: guid(), id: 'notification_recipients',
name: 'notification.recipients', name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link', type: 'link',
path: '/notification/recipients', path: '/notification/recipients',
icon: 'contacts' icon: 'contacts'
}, },
{ {
id: guid(), id: 'notification_templates',
name: 'notification.templates', name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'notification_rules',
name: 'notification.rules', name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog',
@ -184,36 +191,38 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'settings',
name: 'admin.settings', name: 'admin.settings',
type: 'link', type: 'link',
path: '/settings', path: '/settings',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
id: guid(), id: 'general',
name: 'admin.general', name: 'admin.general',
fullName: 'admin.general-settings',
type: 'link', type: 'link',
path: '/settings/general', path: '/settings/general',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: 'mail_server',
name: 'admin.outgoing-mail', name: 'admin.outgoing-mail',
type: 'link', type: 'link',
path: '/settings/outgoing-mail', path: '/settings/outgoing-mail',
icon: 'mail' icon: 'mail'
}, },
{ {
id: guid(), id: 'notification_settings',
name: 'admin.notifications', name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'queues',
name: 'admin.queues', name: 'admin.queues',
type: 'link', type: 'link',
path: '/settings/queues', path: '/settings/queues',
@ -222,21 +231,22 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'security_settings',
name: 'security.security', name: 'security.security',
type: 'toggle', type: 'toggle',
path: '/security-settings', path: '/security-settings',
icon: 'security', icon: 'security',
pages: [ pages: [
{ {
id: guid(), id: 'security_settings_general',
name: 'admin.general', name: 'admin.general',
fullName: 'security.general-settings',
type: 'link', type: 'link',
path: '/security-settings/general', path: '/security-settings/general',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: '2fa',
name: 'admin.2fa.2fa', name: 'admin.2fa.2fa',
type: 'link', type: 'link',
path: '/security-settings/2fa', path: '/security-settings/2fa',
@ -244,7 +254,7 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'oauth2',
name: 'admin.oauth2.oauth2', name: 'admin.oauth2.oauth2',
type: 'link', type: 'link',
path: '/security-settings/oauth2', path: '/security-settings/oauth2',
@ -340,49 +350,49 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'alarms',
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'notifications'
}, },
{ {
id: guid(), id: 'dashboards',
name: 'dashboard.dashboards', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/dashboards', path: '/dashboards',
icon: 'dashboards' icon: 'dashboards'
}, },
{ {
id: guid(), id: 'entities',
name: 'entity.entities', name: 'entity.entities',
type: 'toggle', type: 'toggle',
path: '/entities', path: '/entities',
icon: 'category', icon: 'category',
pages: [ pages: [
{ {
id: guid(), id: 'devices',
name: 'device.devices', name: 'device.devices',
type: 'link', type: 'link',
path: '/entities/devices', path: '/entities/devices',
icon: 'devices_other' icon: 'devices_other'
}, },
{ {
id: guid(), id: 'assets',
name: 'asset.assets', name: 'asset.assets',
type: 'link', type: 'link',
path: '/entities/assets', path: '/entities/assets',
icon: 'domain' icon: 'domain'
}, },
{ {
id: guid(), id: 'entity_views',
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
path: '/entities/entityViews', path: '/entities/entityViews',
@ -391,14 +401,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'profiles',
name: 'profiles.profiles', name: 'profiles.profiles',
type: 'toggle', type: 'toggle',
path: '/profiles', path: '/profiles',
icon: 'badge', icon: 'badge',
pages: [ pages: [
{ {
id: guid(), id: 'device_profiles',
name: 'device-profile.device-profiles', name: 'device-profile.device-profiles',
type: 'link', type: 'link',
path: '/profiles/deviceProfiles', path: '/profiles/deviceProfiles',
@ -406,7 +416,7 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'asset_profiles',
name: 'asset-profile.asset-profiles', name: 'asset-profile.asset-profiles',
type: 'link', type: 'link',
path: '/profiles/assetProfiles', path: '/profiles/assetProfiles',
@ -416,14 +426,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'customers',
name: 'customer.customers', name: 'customer.customers',
type: 'link', type: 'link',
path: '/customers', path: '/customers',
icon: 'supervisor_account' icon: 'supervisor_account'
}, },
{ {
id: guid(), id: 'rule_chains',
name: 'rulechain.rulechains', name: 'rulechain.rulechains',
type: 'link', type: 'link',
path: '/ruleChains', path: '/ruleChains',
@ -433,22 +443,24 @@ export class MenuService {
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{ {
id: guid(), id: 'edge_management',
name: 'edge.management', name: 'edge.management',
type: 'toggle', type: 'toggle',
path: '/edgeManagement', path: '/edgeManagement',
icon: 'settings_input_antenna', icon: 'settings_input_antenna',
pages: [ pages: [
{ {
id: guid(), id: 'edges',
name: 'edge.instances', name: 'edge.instances',
fullName: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edgeManagement/instances', path: '/edgeManagement/instances',
icon: 'router' icon: 'router'
}, },
{ {
id: guid(), id: 'rulechain_templates',
name: 'edge.rulechain-templates', name: 'edge.rulechain-templates',
fullName: 'edge.edge-rulechain-templates',
type: 'link', type: 'link',
path: '/edgeManagement/ruleChains', path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet' icon: 'settings_ethernet'
@ -459,21 +471,21 @@ export class MenuService {
} }
sections.push( sections.push(
{ {
id: guid(), id: 'features',
name: 'feature.advanced-features', name: 'feature.advanced-features',
type: 'toggle', type: 'toggle',
path: '/features', path: '/features',
icon: 'construction', icon: 'construction',
pages: [ pages: [
{ {
id: guid(), id: 'otaUpdates',
name: 'ota-update.ota-updates', name: 'ota-update.ota-updates',
type: 'link', type: 'link',
path: '/features/otaUpdates', path: '/features/otaUpdates',
icon: 'memory' icon: 'memory'
}, },
{ {
id: guid(), id: 'version_control',
name: 'version-control.version-control', name: 'version-control.version-control',
type: 'link', type: 'link',
path: '/features/vc', path: '/features/vc',
@ -482,21 +494,21 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'resources',
name: 'admin.resources', name: 'admin.resources',
type: 'toggle', type: 'toggle',
path: '/resources', path: '/resources',
icon: 'folder', icon: 'folder',
pages: [ pages: [
{ {
id: guid(), id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-bundles',
icon: 'now_widgets' icon: 'now_widgets'
}, },
{ {
id: guid(), id: 'resources_library',
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
@ -506,7 +518,7 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -514,37 +526,42 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
}, },
{ {
id: guid(), id: 'notification_sent',
name: 'notification.sent', name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link', type: 'link',
path: '/notification/sent', path: '/notification/sent',
icon: 'outbox' icon: 'outbox'
}, },
{ {
id: guid(), id: 'notification_recipients',
name: 'notification.recipients', name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link', type: 'link',
path: '/notification/recipients', path: '/notification/recipients',
icon: 'contacts' icon: 'contacts'
}, },
{ {
id: guid(), id: 'notification_templates',
name: 'notification.templates', name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'notification_rules',
name: 'notification.rules', name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog',
@ -553,44 +570,48 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'api_usage',
name: 'api-usage.api-usage', name: 'api-usage.api-usage',
type: 'link', type: 'link',
path: '/usage', path: '/usage',
icon: 'insert_chart' icon: 'insert_chart'
}, },
{ {
id: guid(), id: 'settings',
name: 'admin.settings', name: 'admin.settings',
type: 'link', type: 'link',
path: '/settings', path: '/settings',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
id: guid(), id: 'home_settings',
name: 'admin.home', name: 'admin.home',
fullName: 'admin.home-settings',
type: 'link', type: 'link',
path: '/settings/home', path: '/settings/home',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: 'notification_settings',
name: 'admin.notifications', name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'repository_settings',
name: 'admin.repository', name: 'admin.repository',
fullName: 'admin.repository-settings',
type: 'link', type: 'link',
path: '/settings/repository', path: '/settings/repository',
icon: 'manage_history' icon: 'manage_history'
}, },
{ {
id: guid(), id: 'auto_commit_settings',
name: 'admin.auto-commit', name: 'admin.auto-commit',
fullName: 'admin.auto-commit-settings',
type: 'link', type: 'link',
path: '/settings/auto-commit', path: '/settings/auto-commit',
icon: 'settings_backup_restore' icon: 'settings_backup_restore'
@ -598,14 +619,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'security_settings',
name: 'security.security', name: 'security.security',
type: 'toggle', type: 'toggle',
path: '/security-settings', path: '/security-settings',
icon: 'security', icon: 'security',
pages: [ pages: [
{ {
id: guid(), id: 'audit_log',
name: 'audit-log.audit-logs', name: 'audit-log.audit-logs',
type: 'link', type: 'link',
path: '/security-settings/auditLogs', path: '/security-settings/auditLogs',
@ -781,49 +802,49 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'alarms',
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'notifications'
}, },
{ {
id: guid(), id: 'dashboards',
name: 'dashboard.dashboards', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/dashboards', path: '/dashboards',
icon: 'dashboards' icon: 'dashboards'
}, },
{ {
id: guid(), id: 'entities',
name: 'entity.entities', name: 'entity.entities',
type: 'toggle', type: 'toggle',
path: '/entities', path: '/entities',
icon: 'category', icon: 'category',
pages: [ pages: [
{ {
id: guid(), id: 'devices',
name: 'device.devices', name: 'device.devices',
type: 'link', type: 'link',
path: '/entities/devices', path: '/entities/devices',
icon: 'devices_other' icon: 'devices_other'
}, },
{ {
id: guid(), id: 'assets',
name: 'asset.assets', name: 'asset.assets',
type: 'link', type: 'link',
path: '/entities/assets', path: '/entities/assets',
icon: 'domain' icon: 'domain'
}, },
{ {
id: guid(), id: 'entity_views',
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
path: '/entities/entityViews', path: '/entities/entityViews',
@ -835,8 +856,9 @@ export class MenuService {
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{ {
id: guid(), id: 'edges',
name: 'edge.edge-instances', name: 'edge.edge-instances',
fullName: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edgeManagement/instances', path: '/edgeManagement/instances',
icon: 'router' icon: 'router'
@ -845,7 +867,7 @@ export class MenuService {
} }
sections.push( sections.push(
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -853,8 +875,9 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
@ -928,6 +951,19 @@ export class MenuService {
return homeSections; return homeSections;
} }
private allMenuLinks(sections: Array<MenuSection>): Array<MenuSection> {
const result: Array<MenuSection> = [];
for (const section of sections) {
if (section.type === 'link') {
result.push(section);
}
if (section.pages && section.pages.length) {
result.push(...this.allMenuLinks(section.pages));
}
}
return result;
}
public menuSections(): Observable<Array<MenuSection>> { public menuSections(): Observable<Array<MenuSection>> {
return this.menuSections$; return this.menuSections$;
} }
@ -936,5 +972,24 @@ export class MenuService {
return this.homeSections$; return this.homeSections$;
} }
} public availableMenuLinks(): Observable<Array<MenuSection>> {
return this.availableMenuLinks$;
}
public menuLinkById(id: string): Observable<MenuSection | undefined> {
return this.availableMenuLinks$.pipe(
map((links) => links.find(link => link.id === id))
);
}
public menuLinksByIds(ids: string[]): Observable<Array<MenuSection>> {
return this.availableMenuLinks$.pipe(
map((links) => links.filter(link => ids.includes(link.id)).sort((a, b) => {
const i1 = ids.indexOf(a.id);
const i2 = ids.indexOf(b.id);
return i1 - i2;
}))
);
}
}

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;
@ -152,12 +157,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, false);
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,

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

@ -21,12 +21,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
AbstractControl, AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -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 (datasource.type === DatasourceType.entity && 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);

8
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -33,10 +33,10 @@
<span fxFlex="5"></span> <span fxFlex="5"></span>
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> <div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<div class="tb-header-label" translate fxFlex>filter.filter</div> <div class="tb-header-label" translate fxFlex>filter.filter</div>
<div class="tb-header-label" translate fxFlex="120px" <div class="tb-header-label" translate fxFlex="80px"
fxLayout="column" fxLayoutAlign="center center" fxLayout="column" fxLayoutAlign="center center"
style="padding-left: 30px;">filter.editable</div> style="padding-left: 5px;">filter.editable</div>
<div style="min-width: 80px;"></div> <div style="min-width: 132px;"></div>
</div> </div>
</div> </div>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
@ -47,7 +47,7 @@
<span fxFlex="5">{{$index + 1}}.</span> <span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> <div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label> <mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label>
<section fxFlex="120px" style="padding-left: 10px;" <section fxFlex="80px" style="padding-left: 10px;"
class="tb-editable-switch" class="tb-editable-switch"
fxLayout="column" fxLayout="column"
fxLayoutAlign="center center"> fxLayoutAlign="center center">

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,

3
ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts

@ -25,7 +25,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs';
import { distinctUntilChanged, share, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, share, tap } from 'rxjs/operators';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component'; import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component';
@ -47,6 +47,7 @@ export class NotificationBellComponent implements OnDestroy {
count$ = this.countSubject.asObservable().pipe( count$ = this.countSubject.asObservable().pipe(
distinctUntilChanged(), distinctUntilChanged(),
map((value) => value >= 100 ? '99+' : value),
tap(() => setTimeout(() => this.cd.markForCheck())), tap(() => setTimeout(() => this.cd.markForCheck())),
share({ share({
connector: () => new ReplaySubject(1) connector: () => new ReplaySubject(1)

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

1
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -217,6 +217,7 @@ export interface TbFlotKeySettings {
removeFromLegend: boolean; removeFromLegend: boolean;
showLines: boolean; showLines: boolean;
fillLines: boolean; fillLines: boolean;
fillLinesOpacity: number;
showPoints: boolean; showPoints: boolean;
showPointShape: string; showPointShape: string;
pointShapeFormatter: string; pointShapeFormatter: string;

2
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -408,7 +408,7 @@ export class TbFlot {
} }
} }
series.lines = { series.lines = {
fill: keySettings.fillLines === true fill: keySettings.fillLines === true ? (keySettings.fillLinesOpacity || 0.4) : false
}; };
if (this.settings.stack && !this.comparisonEnabled) { if (this.settings.stack && !this.comparisonEnabled) {

33
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html

@ -0,0 +1,33 @@
<!--
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 class="tb-add-quick-link-dialog" [formGroup]="addQuickLinkFormGroup">
<mat-toolbar style="background: transparent;">
<h2 translate>widgets.quick-links.add-link-title</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<tb-quick-link formControlName="link" [addOnly]="true"
(quickLinkAdded)="add($event)">
</tb-quick-link>
</div>
</form>

29
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.scss

@ -0,0 +1,29 @@
/**
* 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.
*/
:host {
.tb-add-quick-link-dialog {
width: 480px;
.mat-toolbar-single-row {
padding: 0 24px;
}
h2 {
color: rgba(0, 0, 0, 0.76);
}
.mat-icon {
color: rgba(0, 0, 0, 0.54);
}
}
}

57
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.ts

@ -0,0 +1,57 @@
///
/// 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, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@Component({
selector: 'tb-add-quick-link-dialog',
templateUrl: './add-quick-link-dialog.component.html',
styleUrls: ['./add-quick-link-dialog.component.scss']
})
export class AddQuickLinkDialogComponent extends
DialogComponent<AddQuickLinkDialogComponent, string> implements OnInit {
addQuickLinkFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddQuickLinkDialogComponent, string>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.addQuickLinkFormGroup = this.fb.group({
link: [null, [Validators.required]]
});
}
cancel(): void {
this.dialogRef.close(null);
}
add(link: string): void {
this.dialogRef.close(link);
}
}

14
ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss

@ -17,23 +17,9 @@
@import "../../../../../../../scss/constants"; @import "../../../../../../../scss/constants";
:host { :host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-title { .tb-title {
margin-top: 8px; margin-top: 8px;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
} }
.tb-info-icon { .tb-info-icon {

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts

@ -30,7 +30,7 @@ import { MediaBreakpoints } from '@shared/models/constants';
@Component({ @Component({
selector: 'tb-configured-features', selector: 'tb-configured-features',
templateUrl: './configured-features.component.html', templateUrl: './configured-features.component.html',
styleUrls: ['./configured-features.component.scss'] styleUrls: ['./home-page-widget.scss', './configured-features.component.scss']
}) })
export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy { export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy {

10
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html

@ -16,7 +16,7 @@
--> -->
<div fxLayout="row" *ngIf="addMode || editMode; else docLinkTemplate"> <div fxLayout="row" *ngIf="addMode || editMode; else docLinkTemplate">
<div fxFlex class="tb-edit-doc-link" [ngClass]="{'edit-mode': editMode}" [formGroup]="editDocLinkFormGroup" fxLayout="column"> <div fxFlex class="tb-edit-link" [ngClass]="{'edit-mode': editMode}" [formGroup]="editDocLinkFormGroup" fxLayout="column">
<tb-material-icon-select required formControlName="icon"></tb-material-icon-select> <tb-material-icon-select required formControlName="icon"></tb-material-icon-select>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.documentation.name</mat-label> <mat-label translate>widgets.documentation.name</mat-label>
@ -44,12 +44,12 @@
</div> </div>
<ng-template #docLinkTemplate> <ng-template #docLinkTemplate>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex class="tb-doc-link"> <div fxFlex class="tb-link">
<div class="tb-doc-container"> <div class="tb-link-container">
<div class="tb-doc-icon-container"> <div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon> <mat-icon color="primary">{{ docLink.icon }}</mat-icon>
</div> </div>
<div class="tb-doc-text">{{ docLink.name }}</div> <div class="tb-link-text">{{ docLink.name }}</div>
</div> </div>
</div> </div>
<div fxLayout="row" fxLayoutAlign="start center" class="tb-edit-buttons"> <div fxLayout="row" fxLayoutAlign="start center" class="tb-edit-buttons">

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.ts

@ -31,7 +31,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
@Component({ @Component({
selector: 'tb-doc-link', selector: 'tb-doc-link',
templateUrl: './doc-link.component.html', templateUrl: './doc-link.component.html',
styleUrls: ['./doc-link.component.scss'], styleUrls: ['./link.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

16
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html

@ -26,20 +26,20 @@
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
</div> </div>
<mat-grid-list class="tb-docs-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize"> <mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
<mat-grid-tile class="tb-docs-tile" *ngFor="let docLink of documentationLinks?.links"> <mat-grid-tile class="tb-links-tile" *ngFor="let docLink of documentationLinks?.links">
<a fxFlex class="tb-doc-button" <a fxFlex class="tb-link-button"
[href]="docLink.link" target="_blank"> [href]="docLink.link" target="_blank">
<div class="tb-doc-container"> <div class="tb-link-container">
<div class="tb-doc-icon-container"> <div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon> <mat-icon color="primary">{{ docLink.icon }}</mat-icon>
</div> </div>
<div class="tb-doc-text">{{ docLink.name }}</div> <div class="tb-link-text">{{ docLink.name }}</div>
</div> </div>
</a> </a>
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile class="tb-docs-tile"> <mat-grid-tile class="tb-links-tile">
<div fxFlex class="tb-add-doc-button" <div fxFlex class="tb-add-link-button"
matTooltip="{{ 'widgets.documentation.add-link' | translate }}" matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="addLink()"> (click)="addLink()">

27
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts

@ -24,16 +24,12 @@ import { DocumentationLink, DocumentationLinks } from '@shared/models/user-setti
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import {
ImportDialogCsvComponent,
ImportDialogCsvData
} from '@home/components/import-export/import-dialog-csv.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component';
import { import {
EditDocLinksDialogComponent, EditLinksDialogComponent,
EditDocLinksDialogData EditLinksDialogData
} from '@home/components/widget/lib/home-page/edit-doc-links-dialog.component'; } from '@home/components/widget/lib/home-page/edit-links-dialog.component';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
@ -88,7 +84,13 @@ const defaultDocLinksMap = new Map<Authority, DocumentationLinks>(
] ]
}], }],
[Authority.CUSTOMER_USER, { [Authority.CUSTOMER_USER, {
links: [] links: [
{
icon: 'rocket',
name: 'Getting started',
link: 'https://thingsboard.io/docs/getting-started-guides/helloworld/'
}
]
}] }]
] ]
); );
@ -100,7 +102,7 @@ interface DocLinksWidgetSettings {
@Component({ @Component({
selector: 'tb-doc-links-widget', selector: 'tb-doc-links-widget',
templateUrl: './doc-links-widget.component.html', templateUrl: './doc-links-widget.component.html',
styleUrls: ['./doc-links-widget.component.scss'] styleUrls: ['./home-page-widget.scss', './links-widget.component.scss']
}) })
export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy {
@ -172,12 +174,13 @@ export class DocLinksWidgetComponent extends PageComponent implements OnInit, On
} }
edit() { edit() {
this.dialog.open<EditDocLinksDialogComponent, EditDocLinksDialogData, this.dialog.open<EditLinksDialogComponent, EditLinksDialogData,
boolean>(EditDocLinksDialogComponent, { boolean>(EditLinksDialogComponent, {
disableClose: true, disableClose: true,
autoFocus: false, autoFocus: false,
data: { data: {
docLinks: this.documentationLinks mode: 'docs',
links: this.documentationLinks
}, },
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe( }).afterClosed().subscribe(

63
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.html

@ -1,63 +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 class="tb-edit-doc-links-dialog" [formGroup]="editDocLinksFormGroup">
<mat-toolbar style="background: transparent;">
<h2 translate>widgets.documentation.title</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="docLinkDrop($event)" [cdkDropListDisabled]="editMode || addMode">
<div cdkDrag class="tb-draggable" *ngFor="let docLinkControl of docLinksFormArray().controls; trackBy: trackByDocLink;
let $index = index; last as isLast;"
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-doc-link fxFlex #docLinkComponent [formControl]="docLinkControl"
[disableEdit]="editMode || addMode"
(editModeChanged)="editMode = $event"
(docLinkUpdated)="update()"
(docLinkDeleted)="deleteLink($index)">
</tb-doc-link>
<div *ngIf="!docLinkComponent.isEditing()"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"
class="tb-drag-handle">
<mat-icon>drag_indicator</mat-icon>
</div>
</div>
</div>
</div>
<div mat-dialog-actions>
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-doc-button"
matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
</div>
<tb-doc-link *ngIf="addMode" fxFlex [(ngModel)]="addingDocLink"
[ngModelOptions]="{standalone: true}"
(docLinkAdded)="linkAdded($event)"
(docLinkAddCanceled)="addMode = false">
</tb-doc-link>
</div>
</form>

114
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.ts

@ -1,114 +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, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { UserSettingsService } from '@core/http/user-settings.service';
export interface EditDocLinksDialogData {
docLinks: DocumentationLinks;
}
@Component({
selector: 'tb-edit-doc-links-dialog',
templateUrl: './edit-doc-links-dialog.component.html',
styleUrls: ['./edit-doc-links-dialog.component.scss']
})
export class EditDocLinksDialogComponent extends
DialogComponent<EditDocLinksDialogComponent, boolean> implements OnInit {
updated = false;
addMode = false;
editMode = false;
docLinks = this.data.docLinks;
addingDocLink: Partial<DocumentationLink>;
editDocLinksFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EditDocLinksDialogData,
public dialogRef: MatDialogRef<EditDocLinksDialogComponent, boolean>,
public fb: UntypedFormBuilder,
private userSettingsService: UserSettingsService) {
super(store, router, dialogRef);
}
ngOnInit(): void {
const docLinksControls: Array<AbstractControl> = [];
for (const docLink of this.docLinks.links) {
docLinksControls.push(this.fb.control(docLink, [Validators.required]));
}
this.editDocLinksFormGroup = this.fb.group({
links: this.fb.array(docLinksControls)
});
}
docLinksFormArray(): UntypedFormArray {
return this.editDocLinksFormGroup.get('links') as UntypedFormArray;
}
trackByDocLink(index: number, docLinkControl: AbstractControl): any {
return docLinkControl;
}
docLinkDrop(event: CdkDragDrop<string[]>) {
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray;
const docLink = docLinksArray.at(event.previousIndex);
docLinksArray.removeAt(event.previousIndex);
docLinksArray.insert(event.currentIndex, docLink);
this.update();
}
addLink() {
this.addingDocLink = { icon: 'notifications' };
this.addMode = true;
}
linkAdded(docLink: DocumentationLink) {
this.addMode = false;
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray;
const docLinkControl = this.fb.control(docLink, [Validators.required]);
docLinksArray.push(docLinkControl);
this.update();
}
deleteLink(index: number) {
(this.editDocLinksFormGroup.get('links') as UntypedFormArray).removeAt(index);
this.update();
}
update() {
if (this.editDocLinksFormGroup.valid) {
const docLinks: DocumentationLinks = this.editDocLinksFormGroup.value;
this.userSettingsService.updateDocumentationLinks(docLinks).subscribe(() => {
this.updated = true;
});
}
}
close(): void {
this.dialogRef.close(this.updated);
}
}

95
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html

@ -0,0 +1,95 @@
<!--
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 class="tb-edit-links-dialog" [formGroup]="editLinksFormGroup">
<mat-toolbar style="background: transparent;">
<h2>{{ (mode === 'docs' ? 'widgets.documentation.title' : 'widgets.quick-links.title') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="linkDrop($event)" [cdkDropListDisabled]="editMode || addMode">
<div cdkDrag class="tb-draggable" *ngFor="let linkControl of linksFormArray().controls; trackBy: trackByLink;
let $index = index; last as isLast;"
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<ng-container [ngSwitch]="mode">
<ng-template [ngSwitchCase]="'docs'">
<tb-doc-link fxFlex #docLink [formControl]="linkControl"
[disableEdit]="editMode || addMode"
(editModeChanged)="editMode = $event"
(docLinkUpdated)="update()"
(docLinkDeleted)="deleteLink($index)">
</tb-doc-link>
<ng-container *ngIf="!docLink.isEditing()">
<ng-container *ngTemplateOutlet="dragHandle"></ng-container>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="'quickLinks'">
<tb-quick-link fxFlex #quickLink [formControl]="linkControl"
[disableEdit]="editMode || addMode"
(editModeChanged)="editMode = $event"
(quickLinkUpdated)="update()"
(quickLinkDeleted)="deleteLink($index)">
</tb-quick-link>
<ng-container *ngIf="!quickLink.isEditing()">
<ng-container *ngTemplateOutlet="dragHandle"></ng-container>
</ng-container>
</ng-template>
</ng-container>
<ng-template #dragHandle>
<div cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"
class="tb-drag-handle">
<mat-icon>drag_indicator</mat-icon>
</div>
</ng-template>
</div>
</div>
</div>
<div mat-dialog-actions>
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-link-button"
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
</div>
<ng-container *ngIf="addMode">
<ng-container [ngSwitch]="mode">
<ng-template [ngSwitchCase]="'docs'">
<tb-doc-link fxFlex [(ngModel)]="addingLink"
[ngModelOptions]="{standalone: true}"
(docLinkAdded)="linkAdded($event)"
(docLinkAddCanceled)="addMode = false">
</tb-doc-link>
</ng-template>
<ng-template [ngSwitchCase]="'quickLinks'">
<tb-quick-link fxFlex [(ngModel)]="addingLink"
[ngModelOptions]="{standalone: true}"
(quickLinkAdded)="linkAdded($event)"
(quickLinkAddCanceled)="addMode = false">
</tb-quick-link>
</ng-template>
</ng-container>
</ng-container>
</div>
</form>

4
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-doc-links-dialog.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.scss

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-edit-doc-links-dialog { .tb-edit-links-dialog {
width: 480px; width: 480px;
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
max-height: 50vh; max-height: 50vh;
@ -42,7 +42,7 @@
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
} }
} }
.tb-add-doc-button { .tb-add-link-button {
height: 55px; height: 55px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;

122
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.ts

@ -0,0 +1,122 @@
///
/// 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, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DocumentationLink, DocumentationLinks, QuickLinks } from '@shared/models/user-settings.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { UserSettingsService } from '@core/http/user-settings.service';
import { Observable } from 'rxjs';
export interface EditLinksDialogData {
mode: 'docs' | 'quickLinks';
links: DocumentationLinks | QuickLinks;
}
@Component({
selector: 'tb-edit-links-dialog',
templateUrl: './edit-links-dialog.component.html',
styleUrls: ['./edit-links-dialog.component.scss']
})
export class EditLinksDialogComponent extends
DialogComponent<EditLinksDialogComponent, boolean> implements OnInit {
updated = false;
addMode = false;
editMode = false;
links = this.data.links;
mode = this.data.mode;
addingLink: Partial<DocumentationLink> | string;
editLinksFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EditLinksDialogData,
public dialogRef: MatDialogRef<EditLinksDialogComponent, boolean>,
public fb: UntypedFormBuilder,
private userSettingsService: UserSettingsService) {
super(store, router, dialogRef);
}
ngOnInit(): void {
const linksControls: Array<AbstractControl> = [];
for (const link of this.links.links) {
linksControls.push(this.fb.control(link, [Validators.required]));
}
this.editLinksFormGroup = this.fb.group({
links: this.fb.array(linksControls)
});
}
linksFormArray(): UntypedFormArray {
return this.editLinksFormGroup.get('links') as UntypedFormArray;
}
trackByLink(index: number, linkControl: AbstractControl): any {
return linkControl;
}
linkDrop(event: CdkDragDrop<string[]>) {
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray;
const link = linksArray.at(event.previousIndex);
linksArray.removeAt(event.previousIndex);
linksArray.insert(event.currentIndex, link);
this.update();
}
addLink() {
this.addingLink = this.mode === 'docs' ? { icon: 'notifications' } : null;
this.addMode = true;
}
linkAdded(link: DocumentationLink | string) {
this.addMode = false;
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray;
const linkControl = this.fb.control(link, [Validators.required]);
linksArray.push(linkControl);
this.update();
}
deleteLink(index: number) {
(this.editLinksFormGroup.get('links') as UntypedFormArray).removeAt(index);
this.update();
}
update() {
if (this.editLinksFormGroup.valid) {
let updateObservable: Observable<void>;
if (this.mode === 'docs') {
updateObservable = this.userSettingsService.updateDocumentationLinks(this.editLinksFormGroup.value);
} else {
updateObservable = this.userSettingsService.updateQuickLinks(this.editLinksFormGroup.value);
}
updateObservable.subscribe(() => {
this.updated = true;
});
}
}
close(): void {
this.dialogRef.close(this.updated);
}
}

123
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html

@ -91,6 +91,129 @@
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }}</a> <mat-icon>description</mat-icon>{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }}</a>
</mat-step> </mat-step>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="authority.TENANT_ADMIN">
<mat-step [completed]="gettingStarted.maxSelectedIndex > -1">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step1.title</div>
<a *ngIf="matStepper.selectedIndex === 0" mat-button color="primary" routerLink="/entities/devices">{{ 'device.devices' | translate }}</a>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step1.content' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-1-provision-device" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }}</a>
</mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 0">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step2.title</div>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-before' | translate | safe: 'html'"></div>
<div class="tb-bordered-content">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false"
[options]="[
{
name: 'Ubuntu',
value: 'ubuntu'
},
{
name: 'MacOS',
value: 'macos'
},
{
name: 'Windows',
value: 'windows'
}
]">
</tb-toggle-header>
<ng-container [ngSwitch]="publishCommandSteps.value">
<ng-template [ngSwitchCase]="'ubuntu'">
<p [innerHTML]="'widgets.getting-started.tenant-admin.step2.ubuntu.install-curl' | translate | safe: 'html'"></p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
sudo apt-get install curl
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'macos'">
<p [innerHTML]="'widgets.getting-started.tenant-admin.step2.macos.install-curl' | translate | safe: 'html'"></p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
brew install curl
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'windows'">
<p [innerHTML]="'widgets.getting-started.tenant-admin.step2.windows.install-curl' | translate | safe: 'html'"></p>
</ng-template>
</ng-container>
<p [innerHTML]="'widgets.getting-started.tenant-admin.step2.replace-access-token' | translate | safe: 'html'"></p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
curl -v -X POST -d &quot;{\&quot;temperature\&quot;: 25}&quot; {{baseUrl}}/api/v1/$ACCESS_TOKEN/telemetry --header &quot;Content-Type:application/json&quot;
{:copy-code}
```
"></tb-markdown>
</div>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-after' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-2-connect-device" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }}</a>
</mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 1">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step3.title</div>
<a *ngIf="matStepper.selectedIndex === 2" mat-button color="primary" routerLink="/dashboards">{{ 'dashboard.dashboards' | translate }}</a>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step3.content' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-3-create-dashboard" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }}</a>
</mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 2">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step4.title</div>
<a *ngIf="matStepper.selectedIndex === 3" mat-button color="primary" routerLink="/profiles/deviceProfiles">{{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }}</a>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step4.content' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-4-configure-alarm-rules" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }}</a>
</mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 3">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step5.title</div>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step5.content-before' | translate | safe: 'html'"></div>
<p [innerHTML]="'widgets.getting-started.tenant-admin.step5.replace-access-token' | translate | safe: 'html'"></p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
curl -v -X POST -d &quot;{\&quot;temperature\&quot;: 26}&quot; {{baseUrl}}/api/v1/$ACCESS_TOKEN/telemetry --header &quot;Content-Type:application/json&quot;
{:copy-code}
```
"></tb-markdown>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step5.content-after' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-5-create-alarm" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }}</a>
</mat-step>
<mat-step [completed]="gettingStarted.maxSelectedIndex > 4">
<ng-template matStepLabel>
<div style="width: 100%;" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.getting-started.tenant-admin.step6.title</div>
<a *ngIf="matStepper.selectedIndex === 5" mat-button color="primary" routerLink="/customers">{{ 'customer.customers' | translate }}</a>
</div>
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step6.content' | translate | safe: 'html'"></div>
<a mat-stroked-button color="primary" href="https://thingsboard.io/docs/getting-started-guides/helloworld/#step-7-assign-device-and-dashboard-to-customer" target="_blank">
<mat-icon>description</mat-icon>{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }}</a>
</mat-step>
</ng-template>
</ng-container> </ng-container>
</mat-stepper> </mat-stepper>
<div *ngIf="allCompleted" fxLayout="column" fxLayoutAlign="center center" style="padding-top: 24px;"> <div *ngIf="allCompleted" fxLayout="column" fxLayoutAlign="center center" style="padding-top: 24px;">

117
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss

@ -83,13 +83,105 @@
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-get-started .mat-vertical-content p { .tb-get-started .mat-vertical-content {
font-style: normal; p, li {
font-weight: 400; font-style: normal;
font-size: 14px; font-weight: 400;
line-height: 20px; font-size: 14px;
letter-spacing: 0.2px; line-height: 20px;
color: rgba(0, 0, 0, 0.54); letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
em {
font-style: normal;
font-weight: 500;
font-size: 13px;
color: rgba(0, 0, 0, 0.38);
}
}
ul {
padding-inline-start: 20px;
}
.tb-bordered-content {
display: flex;
flex-direction: column;
align-items: stretch;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 16px;
p {
margin-top: 16px;
margin-bottom: 8px;
}
}
.tb-markdown-view {
.tb-getting-started-code {
.code-wrapper {
padding: 0;
pre[class*=language-] {
margin: 0;
padding: 9px 38px 9px 16px;
background: rgba(0, 0, 0, 0.03);
border-radius: 6px;
border: none;
}
code[class*="language-"], pre[class*="language-"] {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.38);
overflow: hidden;
white-space: break-spaces;
word-break: break-all;
& * {
color: rgba(0, 0, 0, 0.38);
cursor: inherit;
background: transparent;
}
}
button.clipboard-btn {
right: 0;
height: 34px;
p, div {
background: transparent;
}
p {
margin: 0;
padding: 7px;
color: #305680;
}
div {
top: 0;
padding: 8px;
height: 34px;
width: 34px;
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
-webkit-mask-image: url(/assets/copy-code-icon.svg);
-webkit-mask-repeat: no-repeat;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
}
}
}
}
@media #{$mat-md-lg} {
.tb-bordered-content {
padding: 4px;
}
}
} }
@media #{$mat-md-lg} { @media #{$mat-md-lg} {
@ -107,15 +199,16 @@
line-height: 16px; line-height: 16px;
} }
.tb-get-started .mat-vertical-content p { .tb-get-started .mat-vertical-content {
font-size: 12px; p, li {
line-height: 16px; font-size: 12px;
letter-spacing: 0.25px; line-height: 16px;
letter-spacing: 0.25px;
}
} }
.tb-get-started .mat-vertical-content { .tb-get-started .mat-vertical-content {
padding: 0 16px 16px 16px; padding: 0 16px 16px 16px;
} }
} }
} }

4
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts

@ -27,7 +27,7 @@ import {
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
import { GettingStarted } from '@shared/models/user-settings.models'; import { GettingStarted } from '@shared/models/user-settings.models';
import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper';
import { isUndefined } from '@core/utils'; import { baseUrl, isUndefined } from '@core/utils';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
@ -54,6 +54,8 @@ export class GettingStartedWidgetComponent extends PageComponent implements OnIn
}; };
allCompleted = false; allCompleted = false;
baseUrl = baseUrl();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private userSettingsService: UserSettingsService, private userSettingsService: UserSettingsService,

70
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss

@ -0,0 +1,70 @@
/**
* 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 "../../../../../../../scss/constants";
:host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-title {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
.tb-title-link {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
position: relative;
border-bottom: none;
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
&:hover, &:focus {
border-bottom: none;
}
&::after {
content: 'arrow_forward';
display: inline-block;
transform: rotate(315deg);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 18px;
color: rgba(0, 0, 0, 0.12);
vertical-align: bottom;
margin-left: 6px;
}
&:hover::after {
color: inherit;
}
}
}

30
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts

@ -23,11 +23,19 @@ import { VersionInfoComponent } from '@home/components/widget/lib/home-page/vers
import { DocLinksWidgetComponent } from '@home/components/widget/lib/home-page/doc-links-widget.component'; import { DocLinksWidgetComponent } from '@home/components/widget/lib/home-page/doc-links-widget.component';
import { DocLinkComponent } from '@home/components/widget/lib/home-page/doc-link.component'; import { DocLinkComponent } from '@home/components/widget/lib/home-page/doc-link.component';
import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component'; import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component';
import { EditDocLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-doc-links-dialog.component'; import { EditLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-links-dialog.component';
import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-page/getting-started-widget.component'; import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-page/getting-started-widget.component';
import { import {
GettingStartedCompletedDialogComponent GettingStartedCompletedDialogComponent
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component';
import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component';
import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component';
import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component';
import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component';
import {
RecentDashboardsWidgetComponent
} from '@home/components/widget/lib/home-page/recent-dashboards-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -38,9 +46,15 @@ import {
DocLinksWidgetComponent, DocLinksWidgetComponent,
DocLinkComponent, DocLinkComponent,
AddDocLinkDialogComponent, AddDocLinkDialogComponent,
EditDocLinksDialogComponent, EditLinksDialogComponent,
GettingStartedWidgetComponent, GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
QuickLinkComponent,
AddQuickLinkDialogComponent,
RecentDashboardsWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -53,9 +67,15 @@ import {
DocLinksWidgetComponent, DocLinksWidgetComponent,
DocLinkComponent, DocLinkComponent,
AddDocLinkDialogComponent, AddDocLinkDialogComponent,
EditDocLinksDialogComponent, EditLinksDialogComponent,
GettingStartedWidgetComponent, GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
QuickLinkComponent,
AddQuickLinkDialogComponent,
RecentDashboardsWidgetComponent
] ]
}) })
export class HomePageWidgetsModule { } export class HomePageWidgetsModule { }

33
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss

@ -0,0 +1,33 @@
/**
* 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.
*/
.tb-autocomplete.tb-quick-links {
.mat-mdc-option {
border-bottom: none;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
.mat-icon {
margin-right: 10px;
color: rgba(0, 0, 0, 0.54);
font-size: 20px;
width: 20px;
height: 20px;
}
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-edit-doc-link { .tb-edit-link {
padding: 16px; padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
@ -23,7 +23,7 @@
border: 1px solid rgba(48, 86, 128, 0.32); border: 1px solid rgba(48, 86, 128, 0.32);
} }
} }
.tb-doc-link { .tb-link {
height: 55px; height: 55px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -33,18 +33,18 @@
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04); box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
border-radius: 10px; border-radius: 10px;
.tb-doc-container { .tb-link-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.tb-doc-icon-container { .tb-link-icon-container {
height: 40px; height: 40px;
padding: 8px; padding: 8px;
background: #F3F6FA; background: #F3F6FA;
border-radius: 6px; border-radius: 6px;
margin-right: 8px; margin-right: 8px;
} }
.tb-doc-text { .tb-link-text {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;

52
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss

@ -17,11 +17,6 @@
@import "../../../../../../../scss/constants"; @import "../../../../../../../scss/constants";
:host { :host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-card-header { .tb-card-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -29,39 +24,6 @@
align-items: flex-end; align-items: flex-end;
} }
.tb-title-link {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
position: relative;
border-bottom: none;
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
&:hover, &:focus {
border-bottom: none;
}
&::after {
content: 'arrow_forward';
display: inline-block;
transform: rotate(315deg);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 18px;
color: rgba(0, 0, 0, 0.12);
vertical-align: bottom;
margin-left: 6px;
}
&:hover::after {
color: inherit;
}
}
.tb-title-icon { .tb-title-icon {
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
width: 32px; width: 32px;
@ -69,15 +31,15 @@
padding: 4px; padding: 4px;
} }
.tb-docs-list { .tb-links-list {
overflow: auto; overflow: auto;
} }
.mat-grid-tile.tb-docs-tile { .mat-grid-tile.tb-links-tile {
overflow: visible; overflow: visible;
} }
.tb-doc-button { .tb-link-button {
height: 55px; height: 55px;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
@ -96,11 +58,11 @@
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08); box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08);
} }
.tb-doc-container { .tb-link-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.tb-doc-icon-container { .tb-link-icon-container {
height: 40px; height: 40px;
padding: 8px; padding: 8px;
background: #F3F6FA; background: #F3F6FA;
@ -110,7 +72,7 @@
display: none; display: none;
} }
} }
.tb-doc-text { .tb-link-text {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
@ -127,7 +89,7 @@
} }
} }
.tb-add-doc-button { .tb-add-link-button {
height: 55px; height: 55px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;

91
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html

@ -0,0 +1,91 @@
<!--
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.
-->
<div fxLayout="row" *ngIf="addMode || editMode; else quickLinkTemplate">
<div fxFlex class="tb-edit-link" [ngClass]="{'edit-mode': editMode}" [formGroup]="editQuickLinkFormGroup" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.quick-links.quick-link</mat-label>
<input matInput type="text"
#linkInput
formControlName="link"
(focusin)="onFocus()"
required
[matAutocomplete]="linkAutocomplete">
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete tb-quick-links"
#linkAutocomplete="matAutocomplete"
[displayWith]="displayLinkFn">
<mat-option *ngFor="let link of filteredLinks | async" [value]="link">
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon>
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon>
<span [innerHTML]="link.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null">
<span>
{{ translate.get('widgets.quick-links.no-links-matching', {name: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="editQuickLinkFormGroup.get('link').hasError('required')">
{{ 'widgets.quick-links.quick-link-required' | translate }}
</mat-error>
</mat-form-field>
<div *ngIf="addMode" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="8px">
<button *ngIf="!addOnly" mat-button color="primary" (click)="cancelAdd()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button color="primary" (click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons">
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
</div>
</div>
<ng-template #quickLinkTemplate>
<div fxLayout="row">
<div fxFlex class="tb-link">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon>
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon>
</div>
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="start center" class="tb-edit-buttons">
<button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="switchToEditMode()">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="delete()">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</ng-template>

318
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.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,
EventEmitter,
forwardRef,
Input,
OnInit,
Output,
SkipSelf,
ViewChild
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ErrorStateMatcher } from '@angular/material/core';
import { MenuService } from '@core/services/menu.service';
import { Observable, of } from 'rxjs';
import { MenuSection } from '@core/services/menu.models';
import { TranslateService } from '@ngx-translate/core';
import { catchError, distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-quick-link',
templateUrl: './quick-link.component.html',
styleUrls: ['./link.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuickLinkComponent),
multi: true
},
{provide: ErrorStateMatcher, useExisting: QuickLinkComponent}
]
})
export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher {
@Input()
disabled: boolean;
@Input()
addOnly = false;
@Input()
disableEdit = false;
@Output()
quickLinkAdded = new EventEmitter<string>();
@Output()
quickLinkAddCanceled = new EventEmitter<void>();
@Output()
quickLinkUpdated = new EventEmitter<string>();
@Output()
quickLinkDeleted = new EventEmitter<void>();
@Output()
editModeChanged = new EventEmitter<boolean>();
@ViewChild('linkInput', {static: false}) linkInput: ElementRef;
filteredLinks: Observable<Array<MenuSection>>;
private allLinksObservable$: Observable<Array<MenuSection>> = null;
searchText = '';
editMode = false;
addMode = false;
quickLink: MenuSection;
private propagateChange = null;
public editQuickLinkFormGroup: UntypedFormGroup;
private submitted = false;
private dirty = false;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
private menuService: MenuService,
public translate: TranslateService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store);
}
ngOnInit(): void {
this.addMode = this.addOnly;
this.editQuickLinkFormGroup = this.fb.group({
link: [null, [this.requiredLinkValidator]]
});
this.filteredLinks = this.editQuickLinkFormGroup.get('link').valueChanges
.pipe(
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
}),
map(value => value ? (typeof value === 'string' ? value :
((value as any).translated ? value.name : this.translate.instant(value.name))) : ''),
distinctUntilChanged(),
switchMap(name => this.fetchLinks(name) ),
share()
);
}
requiredLinkValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value || typeof value === 'string') {
return {required: true};
}
return null;
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.editQuickLinkFormGroup.disable({emitEvent: false});
} else {
this.editQuickLinkFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string): void {
if (value) {
this.menuService.menuLinkById(value).subscribe(
(link) => {
this.quickLink = link;
this.editQuickLinkFormGroup.get('link').patchValue(
link, {emitEvent: false}
);
}
);
} else {
this.quickLink = null;
this.editQuickLinkFormGroup.get('link').patchValue(
value, {emitEvent: false}
);
if (!this.editQuickLinkFormGroup.valid) {
this.addMode = true;
this.editModeChanged.emit(true);
}
}
this.dirty = true;
}
updateView(value: MenuSection | null) {
if (this.quickLink !== value) {
this.quickLink = value;
}
}
displayLinkFn = (link?: MenuSection): string | undefined =>
link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined;
fetchLinks(searchText?: string): Observable<Array<MenuSection>> {
this.searchText = searchText;
const pageLink = new PageLink(100, 0, searchText, {
property: 'name',
direction: Direction.ASC
});
return this.getLinks(pageLink).pipe(
catchError(() => of(emptyPageData<MenuSection>())),
map(pageData => pageData.data)
);
}
getLinks(pageLink: PageLink): Observable<PageData<MenuSection>> {
return this.allLinks().pipe(
map((links) => pageLink.filterData(links))
);
}
allLinks(): Observable<Array<MenuSection>> {
if (this.allLinksObservable$ === null) {
this.allLinksObservable$ = this.menuService.availableMenuLinks().pipe(
map((links) => {
const result = deepClone(links);
for (const link of result) {
link.name = this.translate.instant(link.fullName || link.name);
(link as any).translated = true;
}
return result;
}),
publishReplay(1),
refCount()
);
}
return this.allLinksObservable$;
}
onFocus() {
if (this.dirty) {
this.editQuickLinkFormGroup.get('link').updateValueAndValidity({onlySelf: true});
this.dirty = false;
}
}
clear() {
this.editQuickLinkFormGroup.get('link').patchValue('');
setTimeout(() => {
this.linkInput.nativeElement.blur();
this.linkInput.nativeElement.focus();
}, 0);
}
switchToEditMode() {
if (!this.disableEdit && !this.editMode) {
this.submitted = false;
this.editQuickLinkFormGroup.get('link').patchValue(
this.quickLink, {emitEvent: false}
);
this.editMode = true;
this.editModeChanged.emit(true);
}
}
apply() {
this.submitted = true;
this.updateModel();
if (this.quickLink) {
this.editMode = false;
this.editModeChanged.emit(false);
this.quickLinkUpdated.next(this.quickLink.id);
}
}
cancelEdit() {
this.submitted = false;
this.editMode = false;
this.editModeChanged.emit(false);
}
add() {
this.submitted = true;
this.updateModel();
if (this.quickLink) {
if (!this.addOnly) {
this.addMode = false;
this.editModeChanged.emit(false);
}
this.quickLinkAdded.next(this.quickLink.id);
}
}
cancelAdd() {
this.editModeChanged.emit(false);
this.quickLinkAddCanceled.emit();
}
delete() {
this.quickLinkDeleted.emit();
}
isEditing() {
return this.editMode || this.addMode;
}
private updateModel() {
if (this.quickLink) {
this.propagateChange(this.quickLink.id);
} else {
this.propagateChange(null);
}
}
}

51
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html

@ -0,0 +1,51 @@
<!--
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.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.quick-links.title' | translate }}</div>
<button class="tb-title-icon"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
<mat-grid-tile class="tb-links-tile" *ngFor="let quickLink of menuLinks$() | async">
<a fxFlex class="tb-link-button"
[routerLink]="quickLink.path">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
</div>
<div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div>
</div>
</a>
</mat-grid-tile>
<mat-grid-tile class="tb-links-tile">
<div fxFlex class="tb-add-link-button"
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
</div>
</mat-grid-tile>
</mat-grid-list>
</div>

168
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts

@ -0,0 +1,168 @@
///
/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { map, Observable, of, Subscription } from 'rxjs';
import { QuickLinks } from '@shared/models/user-settings.models';
import { UserSettingsService } from '@core/http/user-settings.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models';
import { MatDialog } from '@angular/material/dialog';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { MenuService } from '@core/services/menu.service';
import { MenuSection } from '@core/services/menu.models';
import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component';
import {
EditLinksDialogComponent,
EditLinksDialogData
} from '@home/components/widget/lib/home-page/edit-links-dialog.component';
const defaultQuickLinksMap = new Map<Authority, QuickLinks>(
[
[Authority.SYS_ADMIN, {
links: ['tenants', 'tenant_profiles']
}],
[Authority.TENANT_ADMIN, {
links: ['alarms', 'dashboards', 'devices']
}],
[Authority.CUSTOMER_USER, {
links: ['alarms', 'dashboards', 'devices']
}]
]
);
interface QuickLinksWidgetSettings {
columns: number;
}
@Component({
selector: 'tb-quick-links-widget',
templateUrl: './quick-links-widget.component.html',
styleUrls: ['./home-page-widget.scss', './links-widget.component.scss']
})
export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy {
@Input()
ctx: WidgetContext;
settings: QuickLinksWidgetSettings;
columns: number;
rowHeight = '55px';
gutterSize = '12px';
quickLinks: QuickLinks;
authUser = getCurrentAuthUser(this.store);
private observeBreakpointSubscription: Subscription;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private userSettingsService: UserSettingsService,
private dialog: MatDialog,
private menuService: MenuService,
private breakpointObserver: BreakpointObserver) {
super(store);
}
ngOnInit() {
this.settings = this.ctx.settings;
this.columns = this.settings.columns || 3;
const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']);
this.rowHeight = isMdLg ? '18px' : '55px';
this.gutterSize = isMdLg ? '8px' : '12px';
this.observeBreakpointSubscription = this.breakpointObserver
.observe(MediaBreakpoints['md-lg'])
.subscribe((state: BreakpointState) => {
if (state.matches) {
this.rowHeight = '18px';
this.gutterSize = '8px';
} else {
this.rowHeight = '55px';
this.gutterSize = '12px';
}
this.cd.markForCheck();
}
);
this.loadQuickLinks();
}
ngOnDestroy() {
if (this.observeBreakpointSubscription) {
this.observeBreakpointSubscription.unsubscribe();
}
super.ngOnDestroy();
}
menuLinks$(): Observable<Array<MenuSection>> {
return this.quickLinks ? this.menuService.menuLinksByIds(this.quickLinks.links) : of([]);
}
private loadQuickLinks() {
this.userSettingsService.getQuickLinks().pipe(
map((quickLinks) => {
if (!quickLinks || !quickLinks.links) {
return defaultQuickLinksMap.get(this.authUser.authority);
} else {
return quickLinks;
}
})
).subscribe(
(quickLinks) => {
this.quickLinks = quickLinks;
this.cd.markForCheck();
}
);
}
edit() {
this.dialog.open<EditLinksDialogComponent, EditLinksDialogData,
boolean>(EditLinksDialogComponent, {
disableClose: true,
autoFocus: false,
data: {
mode: 'quickLinks',
links: this.quickLinks
},
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(result) => {
if (result) {
this.loadQuickLinks();
}
});
}
addLink() {
this.dialog.open<AddQuickLinkDialogComponent, any,
string>(AddQuickLinkDialogComponent, {
disableClose: true,
autoFocus: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(link) => {
if (link) {
this.quickLinks.links.push(link);
this.cd.markForCheck();
this.userSettingsService.updateQuickLinks(this.quickLinks).subscribe();
}
});
}
}

106
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -0,0 +1,106 @@
<!--
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.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</div>
</div>
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<ng-template [ngSwitchCase]="'last'">
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;">
<table mat-table
[dataSource]="lastVisitedDashboardsDataSource" matSort
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property"
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="starred">
<mat-header-cell class="star-cell" *matHeaderCellDef>
</mat-header-cell>
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard">
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)"
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.last-viewed' | translate }}
</mat-header-cell>
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard">
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row>
</table>
</div>
<ng-template #noLastVisitedDashboards>
<div class="tb-no-dashboards">
<div class="tb-no-last-visited-bg"></div>
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
</div>
</ng-template>
</ng-template>
<ng-template [ngSwitchCase]="'starred'">
<div style="overflow-y: auto;">
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred">
<div class="tb-cell star-cell">
<mat-icon (click)="toggleDashboardStar(dashboard)"
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</div>
</div>
</div>
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete"
#starDashboardAutocomplete
subscriptSizing="dynamic"
appearance="outline"
[useIdValue]="false"
label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete>
</ng-template>
</ng-container>
<ng-template #loading>
<div class="tb-no-dashboards">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner>
</div>
</ng-template>
</div>

240
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss

@ -0,0 +1,240 @@
/**
* 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 "../../../../../../../scss/constants";
:host {
.tb-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
@media #{$mat-lt-md} {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
}
.tb-no-dashboards {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-last-visited-bg {
width: 60px;
height: 60px;
margin: 10px;
position: relative;
&:before {
content: "";
border-radius: 8px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_bg.svg);
-webkit-mask-repeat: no-repeat;
mask-image: url(/assets/home/no_data_bg.svg);
mask-repeat: no-repeat;
}
}
.tb-no-last-visited-dashboards-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-starred-dashboard-row {
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
@media #{$mat-md-lg} {
height: 34px;
}
}
.tb-cell {
box-sizing: content-box;
&:first-child {
padding: 0 12px;
}
&:nth-child(n+2):nth-last-child(n+2) {
padding: 0 28px 0 0;
}
}
.tb-star-dashboard-autocomplete {
margin-left: 64px;
margin-right: 64px;
@media #{$mat-md-lg} {
margin-left: 48px;
margin-right: 48px;
}
}
}
:host ::ng-deep {
.mat-mdc-cell, .mat-mdc-header-cell, .mdc-data-table__row:last-child .mdc-data-table__cell {
border-bottom: none;
}
.mat-mdc-table {
.mdc-data-table__row, .mdc-data-table__header-row {
height: 40px;
@media #{$mat-md-lg} {
height: 34px;
}
&:hover {
background-color: inherit;
}
}
}
.tb-cell:nth-child(n+2):nth-last-child(n+2) {
padding: 0 28px 0 0;
}
.mat-mdc-row:not(.mat-row-select) .mat-mdc-cell:nth-child(n+2):nth-last-child(n+2) {
@media #{$mat-md-lg} {
padding: 0 12px 0 0;
}
}
.mat-mdc-header-cell, .mat-mdc-cell, .tb-cell {
&.star-cell {
width: 40px;
min-width: 40px;
@media #{$mat-md-lg} {
width: 24px;
min-width: 24px;
max-width: 24px;
}
}
&.title {
width: 100%;
}
&.last-visited {
width: 80px;
@media #{$mat-md-lg} {
width: 50px;
}
}
}
.mat-mdc-header-cell {
font-weight: 400;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.38);
@media #{$mat-md-lg} {
font-size: 11px;
}
}
.mat-mdc-cell.title {
max-width: 0;
}
.mat-mdc-cell, .tb-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
@media #{$mat-md-lg} {
font-size: 11px;
line-height: 16px;
}
&.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
a {
color: rgba(0, 0, 0, 0.87);
border-bottom: none;
&:hover, &:focus {
border-bottom: none;
}
}
}
&.last-visited {
cursor: default;
color: rgba(0, 0, 0, 0.54);
}
.star {
cursor: pointer;
vertical-align: middle;
color: rgba(0, 0, 0, 0.12);
&:hover {
color: rgba(0, 0, 0, 0.18);
}
&.starred {
color: #FAE205;
&:hover {
color: #CDBD2C;
}
}
}
}
.tb-star-dashboard-autocomplete {
.mat-mdc-form-field {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 11px;
line-height: 16px;
}
}
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
padding-top: 4px;
padding-bottom: 4px;
min-height: 28px;
@media #{$mat-md-lg} {
min-height: 24px;
}
}
.mat-mdc-form-field-hint-wrapper {
height: 0;
}
.mdc-text-field--outlined .mdc-notched-outline {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-top-style: dashed;
border-bottom-style: dashed;
}
.mdc-notched-outline__leading {
border-left-style: dashed;
}
.mdc-notched-outline__trailing {
border-right-style: dashed;
}
}
.mat-mdc-icon-button.mat-mdc-button-base {
width: 28px;
height: 28px;
padding: 2px;
}
}
}

222
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts

@ -0,0 +1,222 @@
///
/// 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,
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
QueryList, ViewChild,
ViewChildren
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models';
import {
AbstractUserDashboardInfo,
LastVisitedDashboardInfo, StarredDashboardInfo,
UserDashboardAction,
UserDashboardsInfo
} from '@shared/models/user-settings.models';
import { UserSettingsService } from '@core/http/user-settings.service';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
import { map, share } from 'rxjs/operators';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort';
import { DashboardInfo } from '@shared/models/dashboard.models';
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
@Component({
selector: 'tb-recent-dashboards-widget',
templateUrl: './recent-dashboards-widget.component.html',
styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss']
})
export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@Input()
ctx: WidgetContext;
@ViewChildren(MatSort) lastVisitedDashboardsSort: QueryList<MatSort>;
@ViewChild('starDashboardAutocomplete', {static: false})
starDashboardAutocomplete: DashboardAutocompleteComponent;
authority = Authority;
userDashboardsInfo: UserDashboardsInfo;
authUser = getCurrentAuthUser(this.store);
toggleValue: 'last' | 'starred' = 'last';
lastVisitedDashboardsColumns = ['starred', 'title', 'lastVisited'];
lastVisitedDashboardsDataSource: LastVisitedDashboardsDataSource;
lastVisitedDashboardsPageLink: PageLink;
starredDashboardValue = null;
dirty = false;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private userSettingService: UserSettingsService) {
super(store);
}
ngOnInit() {
this.reload();
}
reload() {
this.userDashboardsInfo = null;
this.cd.markForCheck();
(this.authUser.authority !== Authority.SYS_ADMIN ?
this.userSettingService.getUserDashboardsInfo() : of({last: [], starred: []})).subscribe(
(userDashboardsInfo) => {
this.userDashboardsInfo = userDashboardsInfo;
for (const starredDashboard of this.userDashboardsInfo?.starred) {
starredDashboard.starred = true;
}
this.userDashboardsInfo?.starred.sort((a, b) => a.starredAt - b.starredAt);
if (this.hasLastVisitedDashboards()) {
this.initLastVisitedDashboardsDataSource();
}
this.cd.markForCheck();
}
);
}
toggleValueChange(value: 'last' | 'starred') {
this.toggleValue = value;
if (this.dirty) {
this.dirty = false;
this.reload();
} else {
if (value === 'last' && this.hasLastVisitedDashboards()) {
this.initLastVisitedDashboardsDataSource();
}
}
}
private initLastVisitedDashboardsDataSource() {
this.lastVisitedDashboardsDataSource = new LastVisitedDashboardsDataSource(this.userDashboardsInfo.last);
const sortOrder: SortOrder = {
property: 'lastVisited',
direction: Direction.DESC
};
this.lastVisitedDashboardsPageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder);
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink);
}
ngAfterViewInit() {
this.lastVisitedDashboardsSort.changes.subscribe(() => {
if (this.lastVisitedDashboardsSort.length) {
this.lastVisitedDashboardsSort.get(0).sortChange.subscribe(() =>
this.updateLastVisitedDashboardsData(this.lastVisitedDashboardsSort.get(0)));
}
});
}
updateLastVisitedDashboardsData(sort: MatSort) {
this.lastVisitedDashboardsPageLink.sortOrder.property = sort.active;
this.lastVisitedDashboardsPageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()];
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink);
}
hasLastVisitedDashboards(): boolean {
return !!(this.userDashboardsInfo && this.userDashboardsInfo.last && this.userDashboardsInfo.last.length);
}
toggleDashboardStar(dashboard: AbstractUserDashboardInfo): void {
const action: UserDashboardAction = dashboard.starred ? UserDashboardAction.UNSTAR : UserDashboardAction.STAR;
dashboard.starred = !dashboard.starred;
this.userSettingService.reportUserDashboardAction(dashboard.id, action, {ignoreLoading: true}).subscribe();
this.dirty = true;
if (this.toggleValue === 'starred') {
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id);
if (index > -1) {
this.userDashboardsInfo.starred.splice(index, 1);
this.cd.markForCheck();
}
}
}
onStarDashboard(dashboard: DashboardInfo) {
if (dashboard) {
this.starDashboardAutocomplete.clear();
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id.id);
if (index === -1) {
const starredDashboard: StarredDashboardInfo = {
starredAt: Date.now(),
id: dashboard.id.id,
starred: true,
title: dashboard.title
};
this.userDashboardsInfo.starred.push(starredDashboard);
this.userSettingService.reportUserDashboardAction(dashboard.id.id, UserDashboardAction.STAR, {ignoreLoading: true}).subscribe();
}
this.cd.markForCheck();
}
}
}
export class LastVisitedDashboardsDataSource implements DataSource<LastVisitedDashboardInfo> {
private lastVisitedDashboardsSubject = new BehaviorSubject<LastVisitedDashboardInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<LastVisitedDashboardInfo>>(emptyPageData<LastVisitedDashboardInfo>());
public pageData$ = this.pageDataSubject.asObservable();
constructor(private lastVisitedDashboards: Array<LastVisitedDashboardInfo>) {
}
connect(collectionViewer: CollectionViewer): Observable<LastVisitedDashboardInfo[] | ReadonlyArray<LastVisitedDashboardInfo>> {
return this.lastVisitedDashboardsSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.lastVisitedDashboardsSubject.complete();
this.pageDataSubject.complete();
}
loadData(pageLink: PageLink): void {
const result = pageLink.filterData(this.lastVisitedDashboards);
this.lastVisitedDashboardsSubject.next(result.data);
this.pageDataSubject.next(result);
}
isEmpty(): Observable<boolean> {
return this.lastVisitedDashboardsSubject.pipe(
map((entities) => !entities.length),
share()
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements),
share()
);
}
}

28
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html

@ -0,0 +1,28 @@
<!--
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.
-->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)">
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
</mat-select>
</mat-form-field>
</ng-template>

134
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss

@ -0,0 +1,134 @@
/**
* 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 "../../../../../../../scss/constants";
:host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
width: 100%;
border-radius: 100px;
height: 32px;
padding: 2px;
border: none;
background: rgba(0, 0, 0, 0.06);
.mat-button-toggle + .mat-button-toggle {
border-left: none;
}
.mat-button-toggle.mat-button-toggle-appearance-standard {
flex: 1;
color: rgba(0, 0, 0, 0.38);
background: transparent;
.mat-button-toggle-focus-overlay, .mat-button-toggle-ripple {
border-radius: 20px;
}
.mat-button-toggle-button {
height: 28px;
.mat-button-toggle-label-content {
line-height: 26px;
font-weight: 400;
font-size: 14px;
letter-spacing: 0.2px;
}
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: #F3F6FA;
color: #305680;
border: 1px solid #305680;
border-radius: 20px;
.mat-button-toggle-label-content {
font-weight: 500;
line-height: 24px;
}
}
}
}
}
@media #{$mat-md-lg} {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
height: 24px;
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button {
height: 20px;
display: grid;
.mat-button-toggle-label-content {
line-height: 20px;
font-size: 10px;
padding: 0 2px;
}
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
.mat-button-toggle-label-content {
line-height: 18px;
}
}
}
}
}
}
.tb-toggle-header-select {
&.mat-mdc-form-field {
line-height: 16px;
font-size: 12px;
}
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
min-height: 0;
width: auto;
padding-top: 8px;
padding-bottom: 8px;
}
.mdc-text-field--outlined {
padding-left: 8px;
padding-right: 6px;
}
.mat-mdc-select {
font-weight: 400;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.25px;
}
.mat-mdc-select-value {
color: rgba(0, 0, 0, 0.38);
}
.mat-mdc-select-arrow-wrapper {
height: 12px;
padding-left: 6px;
}
.mat-mdc-select-arrow {
width: 12px;
height: 12px;
& > svg {
display: none;
}
&:after {
font-family: 'Material Icons';
content: "expand_more";
position: absolute;
display: inline-block;
font-size: 20px;
line-height: 12px;
top: 0;
left: -6px;
right: 0;
bottom: 0;
color: rgba(0, 0, 0, 0.38);
}
}
}
}

90
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts

@ -0,0 +1,90 @@
///
/// 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 {
AfterContentInit,
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChildren, EventEmitter,
Input,
OnInit, Output,
QueryList,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { UpdateMessage } from '@shared/models/settings.models';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { of, Subscription } from 'rxjs';
import { MatStepper } from '@angular/material/stepper';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
export interface ToggleHeaderOption {
name: string;
value: any;
}
@Component({
selector: 'tb-toggle-header',
templateUrl: './toggle-header.component.html',
styleUrls: ['./toggle-header.component.scss']
})
export class ToggleHeaderComponent extends PageComponent implements OnInit {
@Input()
value: any;
@Output()
valueChange = new EventEmitter<any>();
@Input()
options: ToggleHeaderOption[];
@Input()
name: string;
@Input()
@coerceBoolean()
useSelectOnMdLg = true;
isMdLg: boolean;
private observeBreakpointSubscription: Subscription;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private breakpointObserver: BreakpointObserver) {
super(store);
}
ngOnInit() {
this.isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']);
this.observeBreakpointSubscription = this.breakpointObserver
.observe(MediaBreakpoints['md-lg'])
.subscribe((state: BreakpointState) => {
this.isMdLg = state.matches;
this.cd.markForCheck();
}
);
}
}

89
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html

@ -0,0 +1,89 @@
<!--
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.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar>
</div>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar>
</div>
</div>
</div>
</ng-template>
</ng-container>
</div>

113
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss

@ -0,0 +1,113 @@
/**
* 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 "../../../../../../../scss/constants";
:host {
.tb-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
@media #{$mat-lt-md} {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
.tb-usage-list {
display: flex;
flex-direction: row;
justify-content: space-between;
overflow-y: auto;
}
.tb-usage-items, .tb-usage-items-counts, .tb-usage-items-progress {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
overflow: hidden;
}
.tb-usage-items-progress {
width: 34px;
@media #{$mat-md} {
display: none;
}
.mdc-linear-progress {
height: 8px;
margin-top: 6px;
margin-bottom: 6px;
border-radius: 2px;
@media #{$mat-md-lg} {
margin-top: 4px;
margin-bottom: 4px;
}
}
}
.tb-usage-items-values {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 16px;
}
.tb-usage-item, .tb-usage-item-counts {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
@media #{$mat-md-lg} {
font-size: 11px;
line-height: 16px;
}
&.critical {
color: #D12730;
}
}
.tb-usage-item {
color: rgba(0, 0, 0, 0.38);
}
.tb-usage-item-counts {
color: rgba(0, 0, 0, 0.76);
}
}
:host ::ng-deep {
.tb-usage-items-progress {
.mat-mdc-progress-bar {
.mdc-linear-progress__bar-inner {
border-top-width: 8px;
}
&.critical {
.mdc-linear-progress__buffer-bar {
background: rgba(209, 39, 48, 0.06);
}
.mdc-linear-progress__bar-inner {
border-top-color: #D12730;
}
}
}
}
}

111
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts

@ -0,0 +1,111 @@
///
/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { of } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models';
import { UsageInfo } from '@shared/models/usage.models';
import { UsageInfoService } from '@core/http/usage-info.service';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
@Component({
selector: 'tb-usage-info-widget',
templateUrl: './usage-info-widget.component.html',
styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss']
})
export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy {
@Input()
ctx: WidgetContext;
usageInfo: UsageInfo;
authUser = getCurrentAuthUser(this.store);
toggleValue: 'entities' | 'apiCalls' = 'entities';
entityItemCritical: {[key: string]: boolean} = {};
apiCallItemCritical: {[key: string]: boolean} = {};
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe,
private usageInfoService: UsageInfoService) {
super(store);
}
ngOnInit() {
(this.authUser.authority === Authority.TENANT_ADMIN ?
this.usageInfoService.getUsageInfo() : of(null)).subscribe(
(usageInfo) => {
this.usageInfo = usageInfo;
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let entitiesHasCriticalItem = false;
let apiCallsHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) {
if (this.entityItemCritical[key]) {
entitiesHasCriticalItem = true;
break;
}
}
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
}
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
);
}
maxValue(max: number): number | string {
return max ? this.shortNumberPipe.transform(max) : '∞';
}
progressValue(value: number, max: number): number {
if (max && value) {
return (value / max) * 100;
}
return 0;
}
private isItemCritical(value: number, max: number): boolean {
if (max && value) {
return (value / max) >= 0.85;
} else {
return false;
}
}
}

18
ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss

@ -17,11 +17,6 @@
@import "../../../../../../../scss/constants"; @import "../../../../../../../scss/constants";
:host { :host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-card-header { .tb-card-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -33,19 +28,6 @@
} }
} }
.tb-title {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
.tb-contact-us { .tb-contact-us {
@media #{$mat-md-lg} { @media #{$mat-md-lg} {
height: 24px; height: 24px;

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts

@ -27,7 +27,7 @@ import { of } from 'rxjs';
@Component({ @Component({
selector: 'tb-version-info', selector: 'tb-version-info',
templateUrl: './version-info.component.html', templateUrl: './version-info.component.html',
styleUrls: ['./version-info.component.scss'] styleUrls: ['./home-page-widget.scss', './version-info.component.scss']
}) })
export class VersionInfoComponent extends PageComponent implements OnInit { export class VersionInfoComponent extends PageComponent implements OnInit {

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html

@ -51,9 +51,13 @@
<mat-label translate>widgets.chart.line-width</mat-label> <mat-label translate>widgets.chart.line-width</mat-label>
<input matInput type="number" min="0" formControlName="lineWidth"> <input matInput type="number" min="0" formControlName="lineWidth">
</mat-form-field> </mat-form-field>
<mat-checkbox fxFlex formControlName="fillLines"> <mat-checkbox formControlName="fillLines">
{{ 'widgets.chart.fill-line' | translate }} {{ 'widgets.chart.fill-line' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.fill-line-opacity</mat-label>
<input matInput type="number" min="0" max="1" step="0.1" formControlName="fillLinesOpacity">
</mat-form-field>
</section> </section>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts

@ -48,6 +48,7 @@ export function flotDataKeyDefaultSettings(chartType: ChartType): TbFlotKeySetti
showLines: chartType === 'graph', showLines: chartType === 'graph',
lineWidth: 1, lineWidth: 1,
fillLines: false, fillLines: false,
fillLinesOpacity: 0.4,
// Points settings // Points settings
showPoints: false, showPoints: false,
@ -146,6 +147,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
showLines: [this.chartType === 'graph', []], showLines: [this.chartType === 'graph', []],
lineWidth: [1, [Validators.min(0)]], lineWidth: [1, [Validators.min(0)]],
fillLines: [false, []], fillLines: [false, []],
fillLinesOpacity: [0.4, [Validators.min(0), Validators.max(1)]],
// Points settings // Points settings
@ -188,15 +190,19 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
}); });
this.flotKeySettingsFormGroup.get('showLines').valueChanges.subscribe(() => { this.flotKeySettingsFormGroup.get('showLines').valueChanges.subscribe(() => {
this.updateValidators(true); this.updateValidators(false);
});
this.flotKeySettingsFormGroup.get('fillLines').valueChanges.subscribe(() => {
this.updateValidators(false);
}); });
this.flotKeySettingsFormGroup.get('showPoints').valueChanges.subscribe(() => { this.flotKeySettingsFormGroup.get('showPoints').valueChanges.subscribe(() => {
this.updateValidators(true); this.updateValidators(false);
}); });
this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').valueChanges.subscribe(() => { this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').valueChanges.subscribe(() => {
this.updateValidators(true); this.updateValidators(false);
}); });
this.flotKeySettingsFormGroup.valueChanges.subscribe(() => { this.flotKeySettingsFormGroup.valueChanges.subscribe(() => {
@ -254,15 +260,22 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
private updateValidators(emitEvent?: boolean): void { private updateValidators(emitEvent?: boolean): void {
const showLines: boolean = this.flotKeySettingsFormGroup.get('showLines').value; const showLines: boolean = this.flotKeySettingsFormGroup.get('showLines').value;
const fillLines: boolean = this.flotKeySettingsFormGroup.get('fillLines').value;
const showPoints: boolean = this.flotKeySettingsFormGroup.get('showPoints').value; const showPoints: boolean = this.flotKeySettingsFormGroup.get('showPoints').value;
const showValuesForComparison: boolean = this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value; const showValuesForComparison: boolean = this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value;
if (showLines) { if (showLines) {
this.flotKeySettingsFormGroup.get('lineWidth').enable({emitEvent}); this.flotKeySettingsFormGroup.get('lineWidth').enable({emitEvent});
this.flotKeySettingsFormGroup.get('fillLines').enable({emitEvent}); this.flotKeySettingsFormGroup.get('fillLines').enable({emitEvent});
if (fillLines) {
this.flotKeySettingsFormGroup.get('fillLinesOpacity').enable({emitEvent});
} else {
this.flotKeySettingsFormGroup.get('fillLinesOpacity').disable({emitEvent});
}
} else { } else {
this.flotKeySettingsFormGroup.get('lineWidth').disable({emitEvent}); this.flotKeySettingsFormGroup.get('lineWidth').disable({emitEvent});
this.flotKeySettingsFormGroup.get('fillLines').disable({emitEvent}); this.flotKeySettingsFormGroup.get('fillLines').disable({emitEvent});
this.flotKeySettingsFormGroup.get('fillLinesOpacity').disable({emitEvent});
} }
if (showPoints) { if (showPoints) {
@ -287,6 +300,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
this.flotKeySettingsFormGroup.get('lineWidth').updateValueAndValidity({emitEvent: false}); this.flotKeySettingsFormGroup.get('lineWidth').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('fillLines').updateValueAndValidity({emitEvent: false}); this.flotKeySettingsFormGroup.get('fillLines').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('fillLinesOpacity').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointsLineWidth').updateValueAndValidity({emitEvent: false}); this.flotKeySettingsFormGroup.get('showPointsLineWidth').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointsRadius').updateValueAndValidity({emitEvent: false}); this.flotKeySettingsFormGroup.get('showPointsRadius').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointShape').updateValueAndValidity({emitEvent: false}); this.flotKeySettingsFormGroup.get('showPointShape').updateValueAndValidity({emitEvent: false});

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html

@ -0,0 +1,23 @@
<!--
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.
-->
<section class="tb-widget-settings" [formGroup]="quickLinksWidgetSettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.quick-links.columns</mat-label>
<input required matInput type="number" step="1" min="1" max="20" formControlName="columns">
</mat-form-field>
</section>

52
ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts

@ -0,0 +1,52 @@
///
/// 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-quick-links-widget-settings',
templateUrl: './quick-links-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent {
quickLinksWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.quickLinksWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
columns: 3
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.quickLinksWidgetSettingsForm = this.fb.group({
columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]]
});
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -262,6 +262,9 @@ import {
import { import {
DocLinksWidgetSettingsComponent DocLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component'; } from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component';
import {
QuickLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -361,7 +364,8 @@ import {
MapWidgetSettingsComponent, MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent, RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent, TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -465,7 +469,8 @@ import {
MapWidgetSettingsComponent, MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent, RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent, TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -533,5 +538,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-map-widget-settings': MapWidgetSettingsComponent, 'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent, 'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent, 'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent 'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent
}; };

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save