Browse Source

Merge branch 'master' of https://github.com/thingsboard/thingsboard into assign-from-alarm-widget

pull/8466/head
Seraphym-Tuhai 3 years ago
parent
commit
89587d74fa
  1. 20
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 26
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  3. 1
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  4. 60
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  5. 108
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  6. 78
      application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
  7. 60
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  8. 77
      application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
  9. 90
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  10. 6
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  11. 3
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  13. 19
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  15. 9
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  16. 72
      application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java
  17. 4
      application/src/main/resources/thingsboard.yml
  18. 31
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  19. 7
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  20. 13
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  21. 35
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  22. 62
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  23. 116
      application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java
  24. 6
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java
  25. 1
      common/cluster-api/src/main/proto/queue.proto
  26. 13
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  27. 9
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java
  28. 36
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfoFilter.java
  29. 2
      common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java
  30. 33
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  31. 4
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java
  32. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java
  33. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
  34. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  35. 19
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  36. 10
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java
  37. 3
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  38. 8
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  39. 64
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  40. 62
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  41. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  42. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ToData.java
  43. 38
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java
  44. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  45. 8
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  46. 28
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  47. 115
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  48. 72
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  49. 25
      dao/src/main/resources/sql/schema-entities.sql
  50. 33
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java
  51. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
  52. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  53. 5
      ui-ngx/src/app/core/auth/auth.models.ts
  54. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  55. 5
      ui-ngx/src/app/core/auth/auth.selectors.ts
  56. 98
      ui-ngx/src/app/core/auth/auth.service.ts
  57. 7
      ui-ngx/src/app/core/http/device.service.ts
  58. 6
      ui-ngx/src/app/core/services/menu.service.ts
  59. 16
      ui-ngx/src/app/core/services/time.service.ts
  60. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  61. 34
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  62. 17
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  63. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  64. 74
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html
  65. 35
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss
  66. 254
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts
  67. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  68. 4
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html
  69. 2
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw
  70. 2
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw
  71. 4
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw
  72. 14
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  73. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  74. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  75. 165
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  76. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
  77. 137
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  78. 68
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
  79. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  80. 88
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  81. 2
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  82. 15
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts
  83. 10
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html
  84. 6
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts
  85. 121
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  86. 2
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  87. 2
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html
  88. 18
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  89. 44
      ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts
  90. 6
      ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw
  91. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  92. 2
      ui-ngx/src/app/shared/models/constants.ts
  93. 128
      ui-ngx/src/app/shared/models/device.models.ts
  94. 2
      ui-ngx/src/app/shared/models/entity-type.models.ts
  95. 3
      ui-ngx/src/app/shared/models/widget.models.ts
  96. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md
  97. 10
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md
  98. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md
  99. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md
  100. 10
      ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md

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

File diff suppressed because one or more lines are too long

26
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -14,6 +14,32 @@
-- limitations under the License. -- limitations under the License.
-- --
-- DEVICE INFO VIEW START
DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_attribute_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(da.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active';
DROP VIEW IF EXISTS device_info_active_ts_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_ts_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(dt.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from ts_kv_dictionary where key = 'active');
-- DEVICE INFO VIEW END
-- USER CREDENTIALS START -- USER CREDENTIALS START
ALTER TABLE user_credentials ALTER TABLE user_credentials

1
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -66,6 +66,7 @@ public class ControllerConstants {
protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page"; protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page";
protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0"; protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0";
protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile"; protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
protected static final String DEVICE_ACTIVE_PARAM_DESCRIPTION = "A boolean value representing the device active flag.";
protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type"; protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type";
protected static final String ASSET_TYPE_DESCRIPTION = "Asset type"; protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'"; protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";

60
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
@ -83,6 +84,7 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ACTIVE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION;
@ -333,6 +335,8 @@ public class DeviceController extends BaseController {
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId, @RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -342,14 +346,15 @@ public class DeviceController extends BaseController {
) throws ThingsboardException { ) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.active(active);
if (type != null && type.trim().length() > 0) { if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink)); filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) { } else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
} }
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
} }
@ApiOperation(value = "Get Tenant Device (getTenantDevice)", @ApiOperation(value = "Get Tenant Device (getTenantDevice)",
@ -415,6 +420,8 @@ public class DeviceController extends BaseController {
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId, @RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -426,14 +433,16 @@ public class DeviceController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.customerId(customerId);
filter.active(active);
if (type != null && type.trim().length() > 0) { if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) { } else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId)); filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
} }
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
} }
@ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
@ -671,7 +680,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody @ResponseBody
public PageData<Device> getEdgeDevices( public PageData<DeviceInfo> getEdgeDevices(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@ -680,6 +689,10 @@ public class DeviceController extends BaseController {
@RequestParam int page, @RequestParam int page,
@ApiParam(value = DEVICE_TYPE_DESCRIPTION) @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -695,25 +708,16 @@ public class DeviceController extends BaseController {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ); checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
PageData<Device> nonFilteredResult; DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.edgeId(edgeId);
filter.active(active);
if (type != null && type.trim().length() > 0) { if (type != null && type.trim().length() > 0) {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); filter.type(type);
} else { } else if (deviceProfileId != null && deviceProfileId.length() > 0) {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink); filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
} }
List<Device> filteredDevices = nonFilteredResult.getData().stream().filter(device -> { return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
try {
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
PageData<Device> filteredResult = new PageData<>(filteredDevices,
nonFilteredResult.getTotalPages(),
nonFilteredResult.getTotalElements(),
nonFilteredResult.hasNext());
return checkNotNull(filteredResult);
} }
@ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)", @ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)",

108
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -76,6 +77,16 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController @RestController
@ -93,7 +104,10 @@ public class NotificationController extends BaseController {
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
@ApiOperation(value = "Get notifications (getNotifications)", @ApiOperation(value = "Get notifications (getNotifications)",
notes = "**WebSocket API**:\n\n" + notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS +
AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
"**WebSocket API**:\n\n" +
"There are 2 types of subscriptions: one for unread notifications count, another for unread notifications themselves.\n\n" + "There are 2 types of subscriptions: one for unread notifications count, another for unread notifications themselves.\n\n" +
"The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" + "The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" +
"Subscription command for unread notifications count:\n" + "Subscription command for unread notifications count:\n" +
@ -144,11 +158,17 @@ public class NotificationController extends BaseController {
"}\n```") "}\n```")
@GetMapping("/notifications") @GetMapping("/notifications")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public PageData<Notification> getNotifications(@RequestParam int pageSize, public PageData<Notification> getNotifications(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Case-insensitive 'substring' filter based on notification subject or text")
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@ApiParam(value = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly, @RequestParam(defaultValue = "false") boolean unreadOnly,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions // no permissions
@ -156,6 +176,9 @@ public class NotificationController extends BaseController {
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
} }
@ApiOperation(value = "Mark notification as read (markNotificationAsRead)",
notes = "Marks notification as read by its id." +
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notification/{id}/read") @PutMapping("/notification/{id}/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markNotificationAsRead(@PathVariable UUID id, public void markNotificationAsRead(@PathVariable UUID id,
@ -165,6 +188,9 @@ public class NotificationController extends BaseController {
notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId); notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId);
} }
@ApiOperation(value = "Mark all notifications as read (markAllNotificationsAsRead)",
notes = "Marks all unread notifications as read." +
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read") @PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) { public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
@ -172,6 +198,9 @@ public class NotificationController extends BaseController {
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId());
} }
@ApiOperation(value = "Delete notification (deleteNotification)",
notes = "Deletes notification by its id." +
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/notification/{id}") @DeleteMapping("/notification/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteNotification(@PathVariable UUID id, public void deleteNotification(@PathVariable UUID id,
@ -181,6 +210,35 @@ public class NotificationController extends BaseController {
notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId); notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId);
} }
@ApiOperation(value = "Create notification request (createNotificationRequest)",
notes = "Processes notification request.\n" +
"Mandatory request properties are `targets` (list of targets ids to send notification to), " +
"and either `templateId` (existing notification template id) or `template` (to send notification without saving the template).\n" +
"Optionally, you can set `sendingDelayInSec` inside the `additionalConfig` field to schedule the notification." + NEW_LINE +
"For each enabled delivery method in the notification template, there must be a target in the `targets` list that supports this delivery method: " +
"if you chose `WEB`, `EMAIL` or `SMS` - there must be at least one target in `targets` of `PLATFORM_USERS` type.\n" +
"For `SLACK` delivery method - you need to chose at least one `SLACK` notification target." + NEW_LINE +
"Notification request object with `PROCESSING` status will be returned immediately, " +
"and the notification sending itself is done asynchronously. After all notifications are sent, " +
"the `status` of the request becomes `SENT`. Use `getNotificationRequestById` to see " +
"the notification request processing status and some sending stats. " + NEW_LINE +
"Here is an example of notification request to one target using saved template:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"templateId\": {\n" +
" \"entityType\": \"NOTIFICATION_TEMPLATE\",\n" +
" \"id\": \"6dbc3670-e4dd-11ed-9401-dbcc5dff78be\"\n" +
" },\n" +
" \"targets\": [\n" +
" \"320e3ed0-d785-11ed-a06c-21dd57dd88ca\"\n" +
" ],\n" +
" \"additionalConfig\": {\n" +
" \"sendingDelayInSec\": 0\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH
)
@PostMapping("/notification/request") @PostMapping("/notification/request")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequest createNotificationRequest(@RequestBody @Valid NotificationRequest notificationRequest, public NotificationRequest createNotificationRequest(@RequestBody @Valid NotificationRequest notificationRequest,
@ -200,9 +258,15 @@ public class NotificationController extends BaseController {
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null)); return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null));
} }
@ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)",
notes = "Returns preview for notification request." + NEW_LINE +
"`processedTemplates` shows how the notifications for each delivery method will look like " +
"for the first recipient of the corresponding notification target." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/notification/request/preview") @PostMapping("/notification/request/preview")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request, public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request,
@ApiParam(value = "Amount of the recipients to show in preview")
@RequestParam(defaultValue = "20") int recipientsPreviewSize, @RequestParam(defaultValue = "20") int recipientsPreviewSize,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationTemplate template; NotificationTemplate template;
@ -281,6 +345,9 @@ public class NotificationController extends BaseController {
return preview; return preview;
} }
@ApiOperation(value = "Get notification request by id (getNotificationRequestById)",
notes = "Fetches notification request info by request id." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/notification/request/{id}") @GetMapping("/notification/request/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException { public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException {
@ -288,12 +355,21 @@ public class NotificationController extends BaseController {
return checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestInfoById, Operation.READ); return checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestInfoById, Operation.READ);
} }
@ApiOperation(value = "Get notification requests (getNotificationRequests)",
notes = "Returns the page of notification requests submitted by users of this tenant or sysadmins." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/notification/requests") @GetMapping("/notification/requests")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationRequestInfo> getNotificationRequests(@RequestParam int pageSize, public PageData<NotificationRequestInfo> getNotificationRequests(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Case-insensitive 'substring' filed based on the used template name")
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission // generic permission
@ -301,6 +377,11 @@ public class NotificationController extends BaseController {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(user.getTenantId(), EntityType.USER, pageLink); return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(user.getTenantId(), EntityType.USER, pageLink);
} }
@ApiOperation(value = "Delete notification request (deleteNotificationRequest)",
notes = "Deletes notification request by its id." + NEW_LINE +
"If the request has status `SENT` - all sent notifications for this request will be deleted. " +
"If it is `SCHEDULED`, the request will be cancelled." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/notification/request/{id}") @DeleteMapping("/notification/request/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationRequest(@PathVariable UUID id) throws Exception { public void deleteNotificationRequest(@PathVariable UUID id) throws Exception {
@ -310,6 +391,21 @@ public class NotificationController extends BaseController {
} }
@ApiOperation(value = "Save notification settings (saveNotificationSettings)",
notes = "Saves notification settings for this tenant or sysadmin.\n" +
"`deliveryMethodsConfigs` of the settings must be specified." + NEW_LINE +
"Here is an example of the notification settings with Slack configuration:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"deliveryMethodsConfigs\": {\n" +
" \"SLACK\": {\n" +
" \"method\": \"SLACK\",\n" +
" \"botToken\": \"xoxb-....\"\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/notification/settings") @PostMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings, public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings,
@ -320,6 +416,9 @@ public class NotificationController extends BaseController {
return notificationSettings; return notificationSettings;
} }
@ApiOperation(value = "Get notification settings (getNotificationSettings)",
notes = "Retrieves notification settings for this tenant or sysadmin." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/notification/settings") @GetMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
@ -328,6 +427,9 @@ public class NotificationController extends BaseController {
return notificationSettingsService.findNotificationSettings(tenantId); return notificationSettingsService.findNotificationSettings(tenantId);
} }
@ApiOperation(value = "Get available delivery methods (getAvailableDeliveryMethods)",
notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/notification/deliveryMethods") @GetMapping("/notification/deliveryMethods")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {

78
application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -44,6 +46,15 @@ import org.thingsboard.server.service.security.permission.Operation;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController @RestController
@ -55,6 +66,53 @@ public class NotificationRuleController extends BaseController {
private final NotificationRuleService notificationRuleService; private final NotificationRuleService notificationRuleService;
@ApiOperation(value = "Save notification rule (saveNotificationRule)",
notes = "Creates or updates notification rule. " + NEW_LINE +
"Mandatory properties are `name`, `templateId` (of a template with `notificationType` matching to rule's `triggerType`), " +
"`triggerType`, `triggerConfig` and `recipientConfig`. Additionally, you may specify rule `description` " +
"inside of `additionalConfig`." + NEW_LINE +
"Trigger type of the rule cannot be changed. " +
"Available trigger types for tenant: `ENTITY_ACTION`, `ALARM`, `ALARM_COMMENT`, `ALARM_ASSIGNMENT`, " +
"`DEVICE_ACTIVITY`, `RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT`.\n" +
"For sysadmin, there are following trigger types available: `ENTITIES_LIMIT`, `API_USAGE_LIMIT`, " +
"`NEW_PLATFORM_VERSION`." + NEW_LINE +
"Here is an example of notification rule to send notification when a " +
"device, asset or customer is created or deleted:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"name\": \"Entity action\",\n" +
" \"templateId\": {\n" +
" \"entityType\": \"NOTIFICATION_TEMPLATE\",\n" +
" \"id\": \"32117320-d785-11ed-a06c-21dd57dd88ca\"\n" +
" },\n" +
" \"triggerType\": \"ENTITY_ACTION\",\n" +
" \"triggerConfig\": {\n" +
" \"entityTypes\": [\n" +
" \"CUSTOMER\",\n" +
" \"DEVICE\",\n" +
" \"ASSET\"\n" +
" ],\n" +
" \"created\": true,\n" +
" \"updated\": false,\n" +
" \"deleted\": true,\n" +
" \"triggerType\": \"ENTITY_ACTION\"\n" +
" },\n" +
" \"recipientsConfig\": {\n" +
" \"targets\": [\n" +
" \"320f2930-d785-11ed-a06c-21dd57dd88ca\"\n" +
" ],\n" +
" \"triggerType\": \"ENTITY_ACTION\"\n" +
" },\n" +
" \"additionalConfig\": {\n" +
" \"description\": \"Send notification to tenant admins or customer users when a device, asset or customer is created\"\n" +
" },\n" +
" \"templateName\": \"Entity action notification\",\n" +
" \"deliveryMethods\": [\n" +
" \"WEB\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/rule") @PostMapping("/rule")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule, public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule,
@ -74,6 +132,11 @@ public class NotificationRuleController extends BaseController {
return notificationRule; return notificationRule;
} }
@ApiOperation(value = "Get notification rule by id (getNotificationRuleById)",
notes = "Fetches notification rule info by rule's id.\n" +
"In addition to regular notification rule fields, " +
"there are `templateName` and `deliveryMethods` in the response." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/rule/{id}") @GetMapping("/rule/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException { public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException {
@ -81,12 +144,21 @@ public class NotificationRuleController extends BaseController {
return checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleInfoById, Operation.READ); return checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleInfoById, Operation.READ);
} }
@ApiOperation(value = "Get notification rules (getNotificationRules)",
notes = "Returns the page of notification rules." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/rules") @GetMapping("/rules")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationRuleInfo> getNotificationRules(@RequestParam int pageSize, public PageData<NotificationRuleInfo> getNotificationRules(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Case-insensitive 'substring' filter based on rule's name")
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission // generic permission
@ -94,6 +166,10 @@ public class NotificationRuleController extends BaseController {
return notificationRuleService.findNotificationRulesInfosByTenantId(user.getTenantId(), pageLink); return notificationRuleService.findNotificationRulesInfosByTenantId(user.getTenantId(), pageLink);
} }
@ApiOperation(value = "Delete notification rule (deleteNotificationRule)",
notes = "Deletes notification rule by id.\n" +
"Cancels all related scheduled notification requests (e.g. due to escalation table)" +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/rule/{id}") @DeleteMapping("/rule/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationRule(@PathVariable UUID id, public void deleteNotificationRule(@PathVariable UUID id,

60
application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
@ -58,6 +59,14 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@ -71,7 +80,28 @@ public class NotificationTargetController extends BaseController {
private final NotificationTargetService notificationTargetService; private final NotificationTargetService notificationTargetService;
@ApiOperation(value = "Save notification target (saveNotificationTarget)", @ApiOperation(value = "Save notification target (saveNotificationTarget)",
notes = "Create or update notification target." + notes = "Creates or updates notification target." + NEW_LINE +
"Available `configuration` types are `PLATFORM_USERS` and `SLACK`.\n" +
"For `PLATFORM_USERS` the `usersFilter` must be specified. " +
"For tenant, there are following users filter types available: " +
"`USER_LIST`, `CUSTOMER_USERS`, `TENANT_ADMINISTRATORS`, `ALL_USERS`, " +
"`ORIGINATOR_ENTITY_OWNER_USERS`, `AFFECTED_USER`.\n" +
"For sysadmin: `TENANT_ADMINISTRATORS`, `AFFECTED_TENANT_ADMINISTRATORS`, " +
"`SYSTEM_ADMINISTRATORS`, `ALL_USERS`." + NEW_LINE +
"Here is an example of tenant-level notification target to send notification to customer's users:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"name\": \"Users of Customer A\",\n" +
" \"configuration\": {\n" +
" \"type\": \"PLATFORM_USERS\",\n" +
" \"usersFilter\": {\n" +
" \"type\": \"CUSTOMER_USERS\",\n" +
" \"customerId\": \"32499a20-d785-11ed-a06c-21dd57dd88ca\"\n" +
" },\n" +
" \"description\": \"Users of Customer A\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/target") @PostMapping("/target")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -89,7 +119,7 @@ public class NotificationTargetController extends BaseController {
} }
@ApiOperation(value = "Get notification target by id (getNotificationTargetById)", @ApiOperation(value = "Get notification target by id (getNotificationTargetById)",
notes = "Fetch saved notification target by id." + notes = "Fetches notification target by id." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/target/{id}") @GetMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -99,12 +129,14 @@ public class NotificationTargetController extends BaseController {
} }
@ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)", @ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)",
notes = "Get the list (page) of recipients (users) for such notification target configuration." + notes = "Returns the page of recipients for such notification target configuration." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/target/recipients") @PostMapping("/target/recipients")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget, public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission // generic permission
@ -119,9 +151,13 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink);
} }
@ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)",
notes = "Returns the list of notification targets found by provided ids." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets", params = {"ids"}) @GetMapping(value = "/targets", params = {"ids"})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@RequestParam("ids") UUID[] ids, public List<NotificationTarget> getNotificationTargetsByIds(@ApiParam(value = "Comma-separated list of uuids representing targets ids", required = true)
@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
// generic permission // generic permission
List<NotificationTargetId> targetsIds = Arrays.stream(ids).map(NotificationTargetId::new).collect(Collectors.toList()); List<NotificationTargetId> targetsIds = Arrays.stream(ids).map(NotificationTargetId::new).collect(Collectors.toList());
@ -129,14 +165,20 @@ public class NotificationTargetController extends BaseController {
} }
@ApiOperation(value = "Get notification targets (getNotificationTargets)", @ApiOperation(value = "Get notification targets (getNotificationTargets)",
notes = "Fetch the page of notification targets owned by sysadmin or tenant." + notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/targets") @GetMapping("/targets")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargets(@RequestParam int pageSize, public PageData<NotificationTarget> getNotificationTargets(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Case-insensitive 'substring' filed based on the target's name")
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission // generic permission
@ -144,6 +186,10 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink);
} }
@ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)",
notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets", params = "notificationType") @GetMapping(value = "/targets", params = "notificationType")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize,
@ -158,7 +204,7 @@ public class NotificationTargetController extends BaseController {
} }
@ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)",
notes = "Delete notification target by its id.\n\n" + notes = "Deletes notification target by its id." + NEW_LINE +
"This target cannot be referenced by existing scheduled notification requests or any notification rules." + "This target cannot be referenced by existing scheduled notification requests or any notification rules." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/target/{id}") @DeleteMapping("/target/{id}")

77
application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -36,9 +37,9 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -51,6 +52,14 @@ import javax.validation.Valid;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@ -65,19 +74,44 @@ public class NotificationTemplateController extends BaseController {
private final SlackService slackService; private final SlackService slackService;
@ApiOperation(value = "Save notification template (saveNotificationTemplate)", @ApiOperation(value = "Save notification template (saveNotificationTemplate)",
notes = "Create or update notification template.\n\n" + notes = "Creates or updates notification template." + NEW_LINE +
"Example:\n" + "Here is an example of template to send notification via Web, SMS and Slack:\n" +
"```\n{\n \"name\": \"Hello to all my users\",\n" + MARKDOWN_CODE_BLOCK_START +
" \"notificationType\": \"Message from administrator\",\n" + "{\n" +
" \"name\": \"Greetings\",\n" +
" \"notificationType\": \"GENERAL\",\n" +
" \"configuration\": {\n" + " \"configuration\": {\n" +
" \"defaultTextTemplate\": \"Hello everyone\", # required if any of the templates' bodies is not set\n" + " \"deliveryMethodsTemplates\": {\n" +
" \"templates\": {\n" + " \"WEB\": {\n" +
" \"PUSH\": {\n \"method\": \"PUSH\",\n \"body\": null # defaultTextTemplate will be used if body is not set\n },\n" + " \"enabled\": true,\n" +
" \"SMS\": {\n \"method\": \"SMS\",\n \"body\": null\n },\n" + " \"subject\": \"Greetings\",\n" +
" \"EMAIL\": {\n \"method\": \"EMAIL\",\n \"body\": \"Non-default value for email notification: <body>Hello everyone</body>\",\n \"subject\": \"Message from administrator\"\n },\n" + " \"body\": \"Hi there, ${recipientTitle}\",\n" +
" \"SLACK\": {\n \"method\": \"SLACK\",\n \"body\": null,\n \"conversationType\": \"PUBLIC_CHANNEL\",\n \"conversationId\": \"U02LD7BJOU2\" # received from listSlackConversations API method\n }\n" + " \"additionalConfig\": {\n" +
" \"icon\": {\n" +
" \"enabled\": true,\n" +
" \"icon\": \"back_hand\",\n" +
" \"color\": \"#757575\"\n" +
" },\n" +
" \"actionButtonConfig\": {\n" +
" \"enabled\": false\n" +
" }\n" +
" },\n" +
" \"method\": \"WEB\"\n" +
" },\n" +
" \"SMS\": {\n" +
" \"enabled\": true,\n" +
" \"body\": \"Hi there, ${recipientTitle}\",\n" +
" \"method\": \"SMS\"\n" +
" },\n" +
" \"SLACK\": {\n" +
" \"enabled\": true,\n" +
" \"body\": \"Hi there, @${recipientTitle}\",\n" +
" \"method\": \"SLACK\"\n" +
" }\n" +
" }\n" + " }\n" +
" }\n}\n```" + " }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/template") @PostMapping("/template")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -88,7 +122,7 @@ public class NotificationTemplateController extends BaseController {
} }
@ApiOperation(value = "Get notification template by id (getNotificationTemplateById)", @ApiOperation(value = "Get notification template by id (getNotificationTemplateById)",
notes = "Fetch notification template by id." + notes = "Fetches notification template by id." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/template/{id}") @GetMapping("/template/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -98,15 +132,22 @@ public class NotificationTemplateController extends BaseController {
} }
@ApiOperation(value = "Get notification templates (getNotificationTemplates)", @ApiOperation(value = "Get notification templates (getNotificationTemplates)",
notes = "Fetch the page of notification templates owned by sysadmin or tenant." + notes = "Returns the page of notification templates owned by sysadmin or tenant." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/templates") @GetMapping("/templates")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTemplate> getNotificationTemplates(@RequestParam int pageSize, public PageData<NotificationTemplate> getNotificationTemplates(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Case-insensitive 'substring' filter based on template's name and notification type")
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@ApiParam(value = "Comma-separated list of notification types to filter the templates")
@RequestParam(required = false) NotificationType[] notificationTypes, @RequestParam(required = false) NotificationType[] notificationTypes,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission // generic permission
@ -119,7 +160,7 @@ public class NotificationTemplateController extends BaseController {
} }
@ApiOperation(value = "Delete notification template by id (deleteNotificationTemplateById", @ApiOperation(value = "Delete notification template by id (deleteNotificationTemplateById",
notes = "Delete notification template by its id.\n\n" + notes = "Deletes notification template by its id." + NEW_LINE +
"This template cannot be referenced by existing scheduled notification requests or any notification rules." + "This template cannot be referenced by existing scheduled notification requests or any notification rules." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/template/{id}") @DeleteMapping("/template/{id}")
@ -131,12 +172,12 @@ public class NotificationTemplateController extends BaseController {
} }
@ApiOperation(value = "List Slack conversations (listSlackConversations)", @ApiOperation(value = "List Slack conversations (listSlackConversations)",
notes = "List available Slack conversations by type to use in notification template.\n\n" + notes = "List available Slack conversations by type." +
"Slack must be configured in notification settings." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/slack/conversations") @GetMapping("/slack/conversations")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<SlackConversation> listSlackConversations(@RequestParam SlackConversationType type, public List<SlackConversation> listSlackConversations(@RequestParam SlackConversationType type,
@ApiParam(value = "Slack bot token. If absent - system Slack settings will be used")
@RequestParam(required = false) String token, @RequestParam(required = false) String token,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
// generic permission // generic permission

90
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -22,27 +22,59 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.BuildProperties;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import springfox.documentation.annotations.ApiIgnore; import springfox.documentation.annotations.ApiIgnore;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ApiIgnore @ApiIgnore
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping("/api") @RequestMapping("/api")
@Slf4j @Slf4j
public class SystemInfoController { public class SystemInfoController extends BaseController {
@Value("${security.user_token_access_enabled}")
private boolean userTokenAccessEnabled;
@Value("${tbel.enabled:true}")
private boolean tbelEnabled;
@Value("${state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@Value("${ui.dashboard.max_datapoints_limit}")
private long maxDatapointsLimit;
@Autowired(required = false) @Autowired(required = false)
private BuildProperties buildProperties; private BuildProperties buildProperties;
@Autowired
private EntitiesVersionControlService versionControlService;
@PostConstruct @PostConstruct
public void init() { public void init() {
JsonNode info = buildInfoObject(); JsonNode info = buildInfoObject();
@ -56,6 +88,62 @@ public class SystemInfoController {
return buildInfoObject(); return buildInfoObject();
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/system/params", method = RequestMethod.GET)
@ResponseBody
public SystemParams getSystemParams() throws ThingsboardException {
SystemParams systemParams = new SystemParams();
SecurityUser currentUser = getCurrentUser();
TenantId tenantId = currentUser.getTenantId();
CustomerId customerId = currentUser.getCustomerId();
if (currentUser.isSystemAdmin() || currentUser.isTenantAdmin()) {
systemParams.setUserTokenAccessEnabled(userTokenAccessEnabled);
} else {
systemParams.setUserTokenAccessEnabled(false);
}
boolean forceFullscreen = isForceFullscreen(currentUser);
if (forceFullscreen && (currentUser.isTenantAdmin() || currentUser.isCustomerUser())) {
PageLink pageLink = new PageLink(100);
List<DashboardInfo> dashboards;
if (currentUser.isTenantAdmin()) {
dashboards = dashboardService.findDashboardsByTenantId(tenantId, pageLink).getData();
} else {
dashboards = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).getData();
}
systemParams.setAllowedDashboardIds(dashboards.stream().map(d -> d.getId().getId().toString()).collect(Collectors.toList()));
} else {
systemParams.setAllowedDashboardIds(Collections.emptyList());
}
systemParams.setEdgesSupportEnabled(edgesEnabled);
if (currentUser.isTenantAdmin()) {
systemParams.setHasRepository(versionControlService.getVersionControlSettings(tenantId) != null);
systemParams.setTbelEnabled(tbelEnabled);
} else {
systemParams.setHasRepository(false);
systemParams.setTbelEnabled(false);
}
if (currentUser.isTenantAdmin() || currentUser.isCustomerUser()) {
systemParams.setPersistDeviceStateToTelemetry(persistToTelemetry);
} else {
systemParams.setPersistDeviceStateToTelemetry(false);
}
UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL);
ObjectNode userSettingsNode = userSettings == null ? JacksonUtil.newObjectNode() : (ObjectNode) userSettings.getSettings();
if (!userSettingsNode.has("openedMenuSections")) {
userSettingsNode.set("openedMenuSections", JacksonUtil.newArrayNode());
}
systemParams.setUserSettings(userSettingsNode);
systemParams.setMaxDatapointsLimit(maxDatapointsLimit);
return systemParams;
}
private boolean isForceFullscreen(SecurityUser currentUser) {
return UserPrincipal.Type.PUBLIC_ID.equals(currentUser.getUserPrincipal().getType()) ||
(currentUser.getAdditionalInfo() != null &&
currentUser.getAdditionalInfo().has("defaultDashboardFullscreen") &&
currentUser.getAdditionalInfo().get("defaultDashboardFullscreen").booleanValue());
}
private JsonNode buildInfoObject() { private JsonNode buildInfoObject() {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
ObjectNode infoObject = objectMapper.createObjectNode(); ObjectNode infoObject = objectMapper.createObjectNode();

6
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -53,6 +53,9 @@ public class ThingsboardInstallService {
@Value("${install.load_demo:false}") @Value("${install.load_demo:false}")
private Boolean loadDemo; private Boolean loadDemo;
@Value("${state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@Autowired @Autowired
private EntityDatabaseSchemaService entityDatabaseSchemaService; private EntityDatabaseSchemaService entityDatabaseSchemaService;
@ -247,6 +250,7 @@ public class ThingsboardInstallService {
case "3.4.4": case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ..."); log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4"); databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) { if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) {
@ -272,6 +276,8 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseSchema(); entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Installing DataBase schema for timeseries..."); log.info("Installing DataBase schema for timeseries...");
if (noSqlKeyspaceService != null) { if (noSqlKeyspaceService != null) {

3
application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java

@ -16,4 +16,7 @@
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
public interface EntityDatabaseSchemaService extends DatabaseSchemaService { public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry);
} }

6
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -39,4 +39,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
} }
@Override
public void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry) {
String sourceViewName = activityStateInTelemetry ? "device_info_active_ts_view" : "device_info_active_attribute_view";
executeQuery("DROP VIEW IF EXISTS device_info_view CASCADE;");
executeQuery("CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM " + sourceViewName + ";");
}
} }

19
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
@ -294,24 +293,6 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update); return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
} }
@Override
public void sendBasicNotification(TenantId tenantId, UserId recipientId, String subject, String text) {
Notification notification = Notification.builder()
.recipientId(recipientId)
.type(NotificationType.GENERAL)
.subject(subject)
.text(text)
.status(NotificationStatus.SENT)
.build();
notification = notificationService.saveNotification(TenantId.SYS_TENANT_ID, notification);
NotificationUpdate update = NotificationUpdate.builder()
.created(true)
.notification(notification)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
@Override @Override
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) { public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);

1
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -410,6 +410,7 @@ public class DefaultTbClusterService implements TbClusterService {
if (entityType.equals(EntityType.TENANT) if (entityType.equals(EntityType.TENANT)
|| entityType.equals(EntityType.TENANT_PROFILE) || entityType.equals(EntityType.TENANT_PROFILE)
|| entityType.equals(EntityType.DEVICE_PROFILE) || entityType.equals(EntityType.DEVICE_PROFILE)
|| (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| entityType.equals(EntityType.ASSET_PROFILE) || entityType.equals(EntityType.ASSET_PROFILE)
|| entityType.equals(EntityType.API_USAGE_STATE) || entityType.equals(EntityType.API_USAGE_STATE)
|| (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)

9
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -86,6 +86,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -294,7 +295,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return; return;
} }
if (inactivityTimeout <= 0L) { if (inactivityTimeout <= 0L) {
inactivityTimeout = defaultInactivityTimeoutInSec; inactivityTimeout = defaultInactivityTimeoutMs;
} }
log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout); log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout);
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
@ -442,6 +443,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>(); Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>();
partitionedEntities.forEach((tpi, deviceIds) -> { partitionedEntities.forEach((tpi, deviceIds) -> {
log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
Set<DeviceId> idsFromRemovedTenant = new HashSet<>();
for (DeviceId deviceId : deviceIds) { for (DeviceId deviceId : deviceIds) {
DeviceStateData stateData; DeviceStateData stateData;
try { try {
@ -453,9 +455,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
try { try {
updateInactivityStateIfExpired(ts, deviceId, stateData); updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) { if (e instanceof TenantNotFoundException) {
idsFromRemovedTenant.add(deviceId);
continue; continue;
} else {
log.warn("[{}] Failed to update inactivity state [{}]", deviceId, e.getMessage());
} }
} }
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
@ -466,6 +470,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
tenantDevicesActivity.getRight().incrementAndGet(); tenantDevicesActivity.getRight().incrementAndGet();
} }
} }
deviceIds.removeAll(idsFromRemovedTenant);
}); });
devicesActivity.forEach((tenantId, tenantDevicesActivity) -> { devicesActivity.forEach((tenantId, tenantDevicesActivity) -> {
int active = tenantDevicesActivity.getLeft().get(); int active = tenantDevicesActivity.getLeft().get();

72
application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java

@ -21,11 +21,11 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
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.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -61,46 +61,48 @@ public class AlarmsCleanUpService {
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.alarms.checking_interval})}", fixedDelayString = "${sql.ttl.alarms.checking_interval}") @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.alarms.checking_interval})}", fixedDelayString = "${sql.ttl.alarms.checking_interval}")
public void cleanUp() { public void cleanUp() {
PageLink tenantsBatchRequest = new PageLink(10_000, 0); PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
PageLink removalBatchRequest = new PageLink(removalBatchSize, 0 ); for (TenantId tenantId : tenants) {
PageData<TenantId> tenantsIds; try {
do { cleanUp(tenantId);
tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest); } catch (Exception e) {
for (TenantId tenantId : tenantsIds.getData()) { log.warn("Failed to clean up alarms by ttl for tenant {}", tenantId, e);
if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) { }
continue; }
} }
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays()); private void cleanUp(TenantId tenantId) {
long expirationTime = System.currentTimeMillis() - ttl; if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
return;
}
long totalRemoved = 0; Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
while (true) { if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) {
PageData<AlarmId> toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest); return;
toRemove.getData().forEach(alarmId -> { }
relationService.deleteEntityRelations(tenantId, alarmId);
Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm();
entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);
});
totalRemoved += toRemove.getTotalElements(); long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays());
if (!toRemove.hasNext()) { long expirationTime = System.currentTimeMillis() - ttl;
break;
}
}
if (totalRemoved > 0) { PageLink removalBatchRequest = new PageLink(removalBatchSize, 0);
log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime)); long totalRemoved = 0;
while (true) {
PageData<AlarmId> toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest);
for (AlarmId alarmId : toRemove.getData()) {
relationService.deleteEntityRelations(tenantId, alarmId);
Alarm alarm = alarmService.delAlarm(tenantId, alarmId).getAlarm();
if (alarm != null) {
entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);
totalRemoved++;
} }
} }
if (!toRemove.hasNext()) {
tenantsBatchRequest = tenantsBatchRequest.nextPageLink(); break;
} while (tenantsIds.hasNext()); }
}
if (totalRemoved > 0) {
log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
}
} }
} }

4
application/src/main/resources/thingsboard.yml

@ -645,6 +645,10 @@ state:
# Should be greater then transport.sessions.report_timeout # Should be greater then transport.sessions.report_timeout
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}" defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}"
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
# Controls whether we store device 'active' flag in attributes (default) or telemetry.
# If you device to change this parameter, you should re-create the device info view as one of the following:
# If 'persistToTelemetry' is changed from 'false' to 'true': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_ts_view;'
# If 'persistToTelemetry' is changed from 'true' to 'false': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;'
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
tbel: tbel:

31
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -74,6 +74,7 @@ import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
@ -103,16 +104,20 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -123,6 +128,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -202,6 +208,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired @Autowired
private TenantProfileService tenantProfileService; private TenantProfileService tenantProfileService;
@Autowired
private TbTenantProfileService tbTenantProfileService;
@Autowired @Autowired
public TimeseriesService tsService; public TimeseriesService tsService;
@ -903,18 +912,27 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
throw new AssertionError("Unexpected status " + mvcResult.getResponse().getStatus()); throw new AssertionError("Unexpected status " + mvcResult.getResponse().getStatus());
} }
protected <T> T getFieldValue(Object target, String fieldName) throws Exception { protected static <T> T getFieldValue(Object target, String fieldName) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName); Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true); field.setAccessible(true);
return (T) field.get(target); return (T) field.get(target);
} }
protected void setStaticFieldValue(Class<?> targetCls, String fieldName, Object value) throws Exception { protected static void setStaticFieldValue(Class<?> targetCls, String fieldName, Object value) throws Exception {
Field field = targetCls.getDeclaredField(fieldName); Field field = targetCls.getDeclaredField(fieldName);
field.setAccessible(true); field.setAccessible(true);
field.set(null, value); field.set(null, value);
} }
protected static void setStaticFinalFieldValue(Class<?> targetCls, String fieldName, Object value) throws Exception {
Field field = targetCls.getDeclaredField(fieldName);
field.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, value);
}
protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) { protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) {
DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
Map<UUID, SessionInfo> subscriptions = (Map<UUID, SessionInfo>) ReflectionTestUtils.getField(processor, getMapName(featureType)); Map<UUID, SessionInfo> subscriptions = (Map<UUID, SessionInfo>) ReflectionTestUtils.getField(processor, getMapName(featureType));
@ -951,4 +969,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor");
} }
protected void updateDefaultTenantProfile(Consumer<DefaultTenantProfileConfiguration> updater) throws ThingsboardException {
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
updater.accept(profileConfiguration);
tenantProfile.setProfileData(profileData);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
}
} }

7
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -38,6 +38,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -1287,8 +1288,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
savedDevice.getId().getId().toString(), savedEdge.getId().getId().toString(), savedEdge.getName()); savedDevice.getId().getId().toString(), savedEdge.getId().getId().toString(), savedEdge.getName());
testNotificationUpdateGatewayNever(); testNotificationUpdateGatewayNever();
pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?", PageData<DeviceInfo> pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
PAGE_DATA_DEVICE_TYPE_REF, new PageLink(100)); new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
@ -1303,7 +1304,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
testNotificationUpdateGatewayNever(); testNotificationUpdateGatewayNever();
pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?", pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
PAGE_DATA_DEVICE_TYPE_REF, new PageLink(100)); new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(0, pageData.getData().size()); Assert.assertEquals(0, pageData.getData().size());
} }

13
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
@ -322,9 +323,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB()); UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB());
Device device = doGet("/api/device/" + deviceUUID, Device.class); Device device = doGet("/api/device/" + deviceUUID, Device.class);
Assert.assertNotNull(device); Assert.assertNotNull(device);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {}, new PageLink(100)).getData(); new TypeReference<PageData<DeviceInfo>>() {}, new PageLink(100)).getData();
Assert.assertTrue(edgeDevices.contains(device)); Assert.assertTrue(edgeDevices.stream().map(DeviceInfo::getId).anyMatch(id -> id.equals(device.getId())));
testAutoGeneratedCodeByProtobuf(deviceUpdateMsg); testAutoGeneratedCodeByProtobuf(deviceUpdateMsg);
} }
@ -487,10 +488,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
} }
protected Device findDeviceByName(String deviceName) throws Exception { protected Device findDeviceByName(String deviceName) throws Exception {
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() { new TypeReference<PageData<DeviceInfo>>() {
}, new PageLink(100)).getData(); }, new PageLink(100)).getData();
Optional<Device> foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny(); Optional<DeviceInfo> foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny();
Assert.assertTrue(foundDevice.isPresent()); Assert.assertTrue(foundDevice.isPresent());
Device device = foundDevice.get(); Device device = foundDevice.get();
Assert.assertEquals(deviceName, device.getName()); Assert.assertEquals(deviceName, device.getName());

35
application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java

@ -25,11 +25,13 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -46,11 +48,14 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -378,18 +383,18 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}", sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}",
"key2", "value2"); "key2", "value2");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys","key1, inactivityTimeout"); doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2"); doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2");
} }
@Test @Test
public void testSendDeleteDeviceOnEdgeToCloud() throws Exception { public void testSendDeleteDeviceOnEdgeToCloud() throws Exception {
Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge(); Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder(); DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
deviceDeleteMsgBuilder.setIdMSB(device.getId().getId().getMostSignificantBits()); deviceDeleteMsgBuilder.setIdMSB(savedDevice.getId().getId().getMostSignificantBits());
deviceDeleteMsgBuilder.setIdLSB(device.getId().getId().getLeastSignificantBits()); deviceDeleteMsgBuilder.setIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder); testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder);
upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build()); upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build());
@ -398,12 +403,12 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
device = doGet("/api/device/" + device.getUuidId(), Device.class); DeviceInfo deviceInfo = doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class);
Assert.assertNotNull(device); Assert.assertNotNull(deviceInfo);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() { new TypeReference<PageData<DeviceInfo>>() {
}, new PageLink(100)).getData(); }, new PageLink(100)).getData();
Assert.assertFalse(edgeDevices.contains(device)); Assert.assertFalse(edgeDevices.contains(deviceInfo));
} }
@Test @Test
@ -455,7 +460,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value")); Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value"));
String attributeValuesUrl = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/" + DataConstants.SERVER_SCOPE; String attributeValuesUrl = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/" + DataConstants.SERVER_SCOPE;
List<Map<String, String>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {}); List<Map<String, String>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {
});
Assert.assertEquals(3, attributes.size()); Assert.assertEquals(3, attributes.size());
@ -599,7 +605,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS) .atMost(10, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope; String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope;
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {}); List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {
});
return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey); return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey);
}); });
@ -656,7 +663,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
private Map<String, List<Map<String, String>>> loadDeviceTimeseries(Device device, String timeseriesKey) throws Exception { private Map<String, List<Map<String, String>>> loadDeviceTimeseries(Device device, String timeseriesKey) throws Exception {
return doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/values/timeseries?keys=" + timeseriesKey, return doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/values/timeseries?keys=" + timeseriesKey,
new TypeReference<>() {}); new TypeReference<>() {
});
} }
@Test @Test
@ -713,7 +721,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS) .atMost(10, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/timeseries"; String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/timeseries";
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {}); List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {
});
return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains("temperature"); return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains("temperature");
}); });

62
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; 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.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
@ -42,7 +41,6 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
@ -68,15 +66,11 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.apiusage.limits.LimitedApi; import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService; import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
@ -105,10 +99,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationRequestService notificationRequestService; private NotificationRequestService notificationRequestService;
@Autowired @Autowired
private TenantProfileService tenantProfileService;
@Autowired
private TbTenantProfileService tbTenantProfileService;
@Autowired
private RateLimitService rateLimitService; private RateLimitService rateLimitService;
@Autowired @Autowired
private RuleChainService ruleChainService; private RuleChainService ruleChainService;
@ -195,12 +185,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.set("bool", BooleanNode.TRUE); .set("bool", BooleanNode.TRUE);
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(2, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null); .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get(); Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
await().atMost(7, TimeUnit.SECONDS) await().atMost(15, TimeUnit.SECONDS)
.until(() -> clients.values().stream().allMatch(client -> client.getLastDataUpdate() != null)); .until(() -> clients.values().stream().allMatch(client -> client.getLastDataUpdate() != null));
clients.forEach((expectedDelay, wsClient) -> { clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate(); Notification notification = wsClient.getLastDataUpdate().getUpdate();
@ -277,7 +267,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.set("bool", BooleanNode.TRUE); .set("bool", BooleanNode.TRUE);
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(2, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null); .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get(); Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
getWsClient().waitForUpdate(true); getWsClient().waitForUpdate(true);
@ -287,7 +277,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(notification.getInfo()).asInstanceOf(type(AlarmNotificationInfo.class)) assertThat(notification.getInfo()).asInstanceOf(type(AlarmNotificationInfo.class))
.extracting(AlarmNotificationInfo::getAlarmId).isEqualTo(alarm.getUuidId()); .extracting(AlarmNotificationInfo::getAlarmId).isEqualTo(alarm.getUuidId());
await().atMost(2, TimeUnit.SECONDS).until(() -> findNotificationRequests(EntityType.ALARM).getTotalElements() == escalationTable.size()); await().atMost(10, TimeUnit.SECONDS).until(() -> findNotificationRequests(EntityType.ALARM).getTotalElements() == escalationTable.size());
NotificationRequestInfo scheduledNotificationRequest = findNotificationRequests(EntityType.ALARM).getData().stream() NotificationRequestInfo scheduledNotificationRequest = findNotificationRequests(EntityType.ALARM).getData().stream()
.filter(NotificationRequest::isScheduled) .filter(NotificationRequest::isScheduled)
.findFirst().orElse(null); .findFirst().orElse(null);
@ -304,18 +294,15 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationRuleProcessing_entitiesLimit() throws Exception { public void testNotificationRuleProcessing_entitiesLimit() throws Exception {
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
int limit = 5; int limit = 5;
profileConfiguration.setMaxDevices(limit); updateDefaultTenantProfile(profileConfiguration -> {
profileConfiguration.setMaxAssets(limit); profileConfiguration.setMaxDevices(limit);
profileConfiguration.setMaxCustomers(limit); profileConfiguration.setMaxAssets(limit);
profileConfiguration.setMaxUsers(limit); profileConfiguration.setMaxCustomers(limit);
profileConfiguration.setMaxDashboards(limit); profileConfiguration.setMaxUsers(limit);
profileConfiguration.setMaxRuleChains(limit); profileConfiguration.setMaxDashboards(limit);
tenantProfile.setProfileData(profileData); profileConfiguration.setMaxRuleChains(limit);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null); });
EntitiesLimitNotificationRuleTriggerConfig triggerConfig = EntitiesLimitNotificationRuleTriggerConfig.builder() EntitiesLimitNotificationRuleTriggerConfig triggerConfig = EntitiesLimitNotificationRuleTriggerConfig.builder()
.entityTypes(null).threshold(0.8f) .entityTypes(null).threshold(0.8f)
@ -403,18 +390,15 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationRequestsPerRuleRateLimits() throws Exception { public void testNotificationRequestsPerRuleRateLimits() throws Exception {
int notificationRequestsLimit = 10; int notificationRequestsLimit = 10;
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); updateDefaultTenantProfile(profileConfiguration -> {
TenantProfileData profileData = tenantProfile.getProfileData(); profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300");
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); });
profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300");
tenantProfile.setProfileData(profileData);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
NotificationRule rule = new NotificationRule(); NotificationRule rule = new NotificationRule();
rule.setName("Device created"); rule.setName("Device created");
rule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); rule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
NotificationTemplate template = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Device created", "Device created", NotificationTemplate template = createNotificationTemplate(NotificationType.ENTITY_ACTION,
NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.SMS); "Device created", "Device created", NotificationDeliveryMethod.WEB);
rule.setTemplateId(template.getId()); rule.setTemplateId(template.getId());
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
@ -427,14 +411,16 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
rule.setRecipientsConfig(recipientsConfig); rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule); rule = saveNotificationRule(rule);
getWsClient().subscribeForUnreadNotificationsCount().waitForReply();
getWsClient().registerWaitForUpdate(notificationRequestsLimit);
for (int i = 0; i < notificationRequestsLimit; i++) { for (int i = 0; i < notificationRequestsLimit; i++) {
String name = "device " + i; String name = "device " + i;
createDevice(name, name); createDevice(name, name);
TimeUnit.MILLISECONDS.sleep(200);
} }
await().atMost(30, TimeUnit.SECONDS) getWsClient().waitForUpdate(true);
.untilAsserted(() -> { assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(notificationRequestsLimit);
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
});
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
String name = "device " + (notificationRequestsLimit + i); String name = "device " + (notificationRequestsLimit + i);
createDevice(name, name); createDevice(name, name);
@ -444,7 +430,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(rateLimitExceeded).isTrue(); assertThat(rateLimitExceeded).isTrue();
TimeUnit.SECONDS.sleep(3); TimeUnit.SECONDS.sleep(3);
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(notificationRequestsLimit);
} }
private <R> R checkNotificationAfter(Callable<R> action, BiConsumer<Notification, R> check) throws Exception { private <R> R checkNotificationAfter(Callable<R> action, BiConsumer<Notification, R> check) throws Exception {

116
application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java

@ -0,0 +1,116 @@
/**
* 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.service.ttl;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@DaoSqlTest
@TestPropertySource(properties = {
"sql.ttl.alarms.removal_batch_size=5"
})
public class AlarmsCleanUpServiceTest extends AbstractControllerTest {
@Autowired
private AlarmsCleanUpService alarmsCleanUpService;
@SpyBean
private AlarmService alarmService;
@Autowired
private AlarmDao alarmDao;
private static Logger cleanUpServiceLogger;
@BeforeClass
public static void before() throws Exception {
cleanUpServiceLogger = Mockito.spy(LoggerFactory.getLogger(AlarmsCleanUpService.class));
setStaticFinalFieldValue(AlarmsCleanUpService.class, "log", cleanUpServiceLogger);
}
@Test
public void testAlarmsCleanUp() throws Exception {
int ttlDays = 1;
updateDefaultTenantProfile(profileConfiguration -> {
profileConfiguration.setAlarmsTtlDays(ttlDays);
});
loginTenantAdmin();
Device device = createDevice("device_0", "device_0");
int count = 100;
long ts = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(ttlDays) - TimeUnit.MINUTES.toMillis(10);
List<AlarmId> outdatedAlarms = new ArrayList<>();
List<AlarmId> freshAlarms = new ArrayList<>();
for (int i = 0; i < count; i++) {
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(device.getId())
.cleared(false)
.acknowledged(false)
.severity(AlarmSeverity.CRITICAL)
.type("outdated_alarm_" + i)
.startTs(ts)
.endTs(ts)
.build();
alarm.setId(new AlarmId(UUID.randomUUID()));
alarm.setCreatedTime(ts);
outdatedAlarms.add(alarmDao.save(tenantId, alarm).getId());
alarm.setType("fresh_alarm_" + i);
alarm.setStartTs(System.currentTimeMillis());
alarm.setEndTs(alarm.getStartTs());
alarm.setId(new AlarmId(UUID.randomUUID()));
alarm.setCreatedTime(alarm.getStartTs());
freshAlarms.add(alarmDao.save(tenantId, alarm).getId());
}
alarmsCleanUpService.cleanUp();
for (AlarmId outdatedAlarm : outdatedAlarms) {
verify(alarmService).delAlarm(eq(tenantId), eq(outdatedAlarm));
}
for (AlarmId freshAlarm : freshAlarms) {
verify(alarmService, never()).delAlarm(eq(tenantId), eq(freshAlarm));
}
verify(cleanUpServiceLogger).info(startsWith("Removed {} outdated alarm"), eq((long) count), eq(tenantId), any());
}
}

6
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java

@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
@ -409,7 +409,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH(); clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + SHARED_SCOPE;
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.NBIRTH.name()) await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
@ -424,7 +424,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
long ts = calendar.getTimeInMillis(); long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + SHARED_SCOPE;
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.DBIRTH.name()) await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)

1
common/cluster-api/src/main/proto/queue.proto

@ -148,6 +148,7 @@ message PostTelemetryMsg {
message PostAttributeMsg { message PostAttributeMsg {
repeated KeyValueProto kv = 1; repeated KeyValueProto kv = 1;
bool shared = 2;
} }
message GetAttributeRequestMsg { message GetAttributeRequestMsg {

13
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
@ -66,7 +67,7 @@ public interface DeviceService extends EntityDaoService {
PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink); PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink); PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink); PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink);
@ -76,10 +77,6 @@ public interface DeviceService extends EntityDaoService {
Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType); Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds); ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
List<Device> findDevicesByIds(List<DeviceId> deviceIds); List<Device> findDevicesByIds(List<DeviceId> deviceIds);
@ -90,14 +87,8 @@ public interface DeviceService extends EntityDaoService {
PageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); PageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds); ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);

9
common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java

@ -18,18 +18,24 @@ package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ApiModel @ApiModel
@Data @Data
@EqualsAndHashCode(callSuper = true)
public class DeviceInfo extends Device { public class DeviceInfo extends Device {
private static final long serialVersionUID = -3004579925090663691L;
@ApiModelProperty(position = 13, value = "Title of the Customer that owns the device.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 13, value = "Title of the Customer that owns the device.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String customerTitle; private String customerTitle;
@ApiModelProperty(position = 14, value = "Indicates special 'Public' Customer that is auto-generated to use the devices on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 14, value = "Indicates special 'Public' Customer that is auto-generated to use the devices on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private boolean customerIsPublic; private boolean customerIsPublic;
@ApiModelProperty(position = 15, value = "Name of the corresponding Device Profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 15, value = "Name of the corresponding Device Profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String deviceProfileName; private String deviceProfileName;
@ApiModelProperty(position = 16, value = "Device active flag.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private boolean active;
public DeviceInfo() { public DeviceInfo() {
super(); super();
@ -39,10 +45,11 @@ public class DeviceInfo extends Device {
super(deviceId); super(deviceId);
} }
public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName) { public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName, boolean active) {
super(device); super(device);
this.customerTitle = customerTitle; this.customerTitle = customerTitle;
this.customerIsPublic = customerIsPublic; this.customerIsPublic = customerIsPublic;
this.deviceProfileName = deviceProfileName; this.deviceProfileName = deviceProfileName;
this.active = active;
} }
} }

36
common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfoFilter.java

@ -0,0 +1,36 @@
/**
* 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;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@Builder
public class DeviceInfoFilter {
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String type;
private DeviceProfileId deviceProfileId;
private Boolean active;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java

@ -17,9 +17,11 @@ package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
@Data @Data
@EqualsAndHashCode(callSuper = true)
public class EntityViewInfo extends EntityView { public class EntityViewInfo extends EntityView {
@ApiModelProperty(position = 12, value = "Title of the Customer that owns the entity view.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 12, value = "Title of the Customer that owns the entity view.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)

33
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -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.
*/
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import java.util.List;
@Data
public class SystemParams {
boolean userTokenAccessEnabled;
List<String> allowedDashboardIds;
boolean edgesSupportEnabled;
boolean hasRepository;
boolean tbelEnabled;
boolean persistDeviceStateToTelemetry;
JsonNode userSettings;
long maxDatapointsLimit;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java

@ -18,12 +18,16 @@ package org.thingsboard.server.common.data.asset;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ApiModel @ApiModel
@Data @Data
@EqualsAndHashCode(callSuper = true)
public class AssetInfo extends Asset { public class AssetInfo extends Asset {
private static final long serialVersionUID = -4094528227011066194L;
@ApiModelProperty(position = 10, value = "Title of the Customer that owns the asset.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 10, value = "Title of the Customer that owns the asset.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String customerTitle; private String customerTitle;
@ApiModelProperty(position = 11, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 11, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java

@ -55,7 +55,6 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
private NotificationTemplate template; private NotificationTemplate template;
@Valid @Valid
private NotificationInfo info; private NotificationInfo info;
@NotNull
@Valid @Valid
private NotificationRequestConfig additionalConfig; private NotificationRequestConfig additionalConfig;

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java

@ -20,12 +20,12 @@ import lombok.Getter;
@Getter @Getter
public enum NotificationRuleTriggerType { public enum NotificationRuleTriggerType {
ENTITY_ACTION,
ALARM, ALARM,
ALARM_COMMENT, ALARM_COMMENT,
ALARM_ASSIGNMENT,
DEVICE_ACTIVITY, DEVICE_ACTIVITY,
ENTITY_ACTION,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
ALARM_ASSIGNMENT,
NEW_PLATFORM_VERSION(false), NEW_PLATFORM_VERSION(false),
ENTITIES_LIMIT(false), ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false); API_USAGE_LIMIT(false);

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -535,7 +535,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!");
} }
try { try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable e) { } catch (Throwable e) {
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e);

19
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -173,7 +173,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
try { try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable e) { } catch (Throwable e) {
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e);
@ -233,7 +233,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
try { try {
List<TransportProtos.PostTelemetryMsg> msgs = new ArrayList<>(); List<TransportProtos.PostTelemetryMsg> msgs = new ArrayList<>();
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) {
if (attributesMetricNames == null || !attributesMetricNames.contains(protoMetric.getName())) { if (attributesMetricNames == null || !matches(attributesMetricNames, protoMetric)) {
long ts = protoMetric.getTimestamp(); long ts = protoMetric.getTimestamp();
String key = "bdSeq".equals(protoMetric.getName()) ? String key = "bdSeq".equals(protoMetric.getName()) ?
topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); topicTypeName + " " + protoMetric.getName() : protoMetric.getName();
@ -264,7 +264,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
try { try {
List<TransportApiProtos.AttributesMsg> msgs = new ArrayList<>(); List<TransportApiProtos.AttributesMsg> msgs = new ArrayList<>();
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) {
if (attributesMetricNames.contains(protoMetric.getName())) { if (matches(attributesMetricNames, protoMetric)) {
TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder();
Optional<TransportProtos.PostAttributeMsg> msgOpt = getPostAttributeMsg(protoMetric); Optional<TransportProtos.PostAttributeMsg> msgOpt = getPostAttributeMsg(protoMetric);
if (msgOpt.isPresent()) { if (msgOpt.isPresent()) {
@ -281,11 +281,24 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
} }
} }
private boolean matches(Set<String> attributesMetricNames, SparkplugBProto.Payload.Metric protoMetric) {
String metricName = protoMetric.getName();
for (String attributeMetricFilter : attributesMetricNames) {
if (metricName.equals(attributeMetricFilter) ||
(attributeMetricFilter.endsWith("*") && metricName.startsWith(
attributeMetricFilter.substring(0, attributeMetricFilter.length() - 1)))) {
return true;
}
}
return false;
}
private Optional<TransportProtos.PostAttributeMsg> getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { private Optional<TransportProtos.PostAttributeMsg> getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException {
Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric); Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric);
if (keyValueProtoOpt.isPresent()) { if (keyValueProtoOpt.isPresent()) {
TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder();
builder.addKv(keyValueProtoOpt.get()); builder.addKv(keyValueProtoOpt.get());
builder.setShared(true);
return Optional.of(builder.build()); return Optional.of(builder.build());
} }
return Optional.empty(); return Optional.empty();

10
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java

@ -67,13 +67,15 @@ public class ProtoConverter {
} }
} }
public static TransportProtos.PostAttributeMsg validatePostAttributeMsg(byte[] bytes) throws IllegalArgumentException, InvalidProtocolBufferException { public static TransportProtos.PostAttributeMsg validatePostAttributeMsg(TransportProtos.PostAttributeMsg msg) throws IllegalArgumentException, InvalidProtocolBufferException {
TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes); if (!CollectionUtils.isEmpty(msg.getKvList())) {
List<TransportProtos.KeyValueProto> kvList = proto.getKvList(); byte[] bytes = msg.toByteArray();
if (!CollectionUtils.isEmpty(kvList)) { TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes);
List<TransportProtos.KeyValueProto> kvList = proto.getKvList();
List<TransportProtos.KeyValueProto> keyValueProtos = validateKeyValueProtos(kvList); List<TransportProtos.KeyValueProto> keyValueProtos = validateKeyValueProtos(kvList);
TransportProtos.PostAttributeMsg.Builder result = TransportProtos.PostAttributeMsg.newBuilder(); TransportProtos.PostAttributeMsg.Builder result = TransportProtos.PostAttributeMsg.newBuilder();
result.addAllKv(keyValueProtos); result.addAllKv(keyValueProtos);
result.setShared(msg.getShared());
return result.build(); return result.build();
} else { } else {
throw new IllegalArgumentException("KeyValue list is empty!"); throw new IllegalArgumentException("KeyValue list is empty!");

3
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -593,6 +593,9 @@ public class DefaultTransportService implements TransportService {
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("deviceName", sessionInfo.getDeviceName()); metaData.putValue("deviceName", sessionInfo.getDeviceName());
metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("deviceType", sessionInfo.getDeviceType());
if (msg.getShared()) {
metaData.putValue(DataConstants.SCOPE, DataConstants.SHARED_SCOPE);
}
metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "false"); metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "false");
CustomerId customerId = getCustomerId(sessionInfo); CustomerId customerId = getCustomerId(sessionInfo);
sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_ATTRIBUTES_REQUEST, sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_ATTRIBUTES_REQUEST,

8
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -128,4 +129,11 @@ public abstract class DaoUtil {
} while (hasNextBatch); } while (hasNextBatch);
} }
public static String getStringId(UUIDBased id) {
if (id != null) {
return id.toString();
} else {
return null;
}
}
} }

64
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
@ -74,15 +75,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/ */
PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink); PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find device infos by tenantId and page link.
*
* @param tenantId the tenantId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink);
/** /**
* Find devices by tenantId, type and page link. * Find devices by tenantId, type and page link.
* *
@ -100,26 +92,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(UUID tenantId, UUID deviceProfileId, OtaPackageType otaPackageType); Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(UUID tenantId, UUID deviceProfileId, OtaPackageType otaPackageType);
/**
* Find device infos by tenantId, type and page link.
*
* @param tenantId the tenantId
* @param type the type
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, deviceProfileId and page link.
*
* @param tenantId the tenantId
* @param deviceProfileId the deviceProfileId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink);
/** /**
* Find devices by tenantId and devices Ids. * Find devices by tenantId and devices Ids.
* *
@ -155,16 +127,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/ */
PageData<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); PageData<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/**
* Find device infos by tenantId, customerId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/** /**
* Find devices by tenantId, customerId, type and page link. * Find devices by tenantId, customerId, type and page link.
* *
@ -176,28 +138,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/ */
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, customerId, type and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param type the type
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, customerId, deviceProfileId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param deviceProfileId the deviceProfileId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink);
/** /**
* Find devices by tenantId, customerId and devices Ids. * Find devices by tenantId, customerId and devices Ids.
* *
@ -277,5 +217,5 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink); PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
} }

62
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -30,6 +30,7 @@ import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
@ -69,6 +70,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
@ -342,12 +344,17 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
} }
@Override @Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) { public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); log.trace("Executing findDeviceInfosByFilter, filter [{}], pageLink [{}]", filter, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId); if (filter == null) {
throw new IncorrectParameterException("Filter is empty!");
}
validateId(filter.getTenantId(), INCORRECT_TENANT_ID + filter.getTenantId());
validatePageLink(pageLink); validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink); return deviceDao.findDeviceInfosByFilter(filter, pageLink);
} }
@Override @Override
@ -387,24 +394,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type); return deviceDao.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type);
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId.getId(), deviceProfileId.getId(), pageLink);
}
@Override @Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) { public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds); log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
@ -444,15 +433,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
}
@Override @Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
@ -463,26 +443,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId.getId(), customerId.getId(), deviceProfileId.getId(), pageLink);
}
@Override @Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) { public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);

7
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -169,6 +169,11 @@ public class ModelConstants {
public static final String DEVICE_FIRMWARE_ID_PROPERTY = "firmware_id"; public static final String DEVICE_FIRMWARE_ID_PROPERTY = "firmware_id";
public static final String DEVICE_SOFTWARE_ID_PROPERTY = "software_id"; public static final String DEVICE_SOFTWARE_ID_PROPERTY = "software_id";
public static final String DEVICE_CUSTOMER_TITLE_PROPERTY = "customer_title";
public static final String DEVICE_CUSTOMER_IS_PUBLIC_PROPERTY = "customer_is_public";
public static final String DEVICE_DEVICE_PROFILE_NAME_PROPERTY = "device_profile_name";
public static final String DEVICE_ACTIVE_PROPERTY = "active";
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
@ -176,6 +181,8 @@ public class ModelConstants {
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
public static final String DEVICE_INFO_VIEW_COLUMN_FAMILY_NAME = "device_info_view";
/** /**
* Device profile constants. * Device profile constants.
*/ */

1
dao/src/main/java/org/thingsboard/server/dao/model/ToData.java

@ -28,4 +28,5 @@ public interface ToData<T> {
* @return the dto object * @return the dto object
*/ */
T toData(); T toData();
} }

38
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java

@ -15,48 +15,40 @@
*/ */
package org.thingsboard.server.dao.model.sql; package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.HashMap; import javax.persistence.Column;
import java.util.Map; import javax.persistence.Entity;
import javax.persistence.Table;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Entity
@Immutable
@Table(name = ModelConstants.DEVICE_INFO_VIEW_COLUMN_FAMILY_NAME)
public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> { public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> {
public static final Map<String,String> deviceInfoColumnMap = new HashMap<>(); @Column(name = ModelConstants.DEVICE_CUSTOMER_TITLE_PROPERTY)
static {
deviceInfoColumnMap.put("customerTitle", "c.title");
deviceInfoColumnMap.put("deviceProfileName", "p.name");
}
private String customerTitle; private String customerTitle;
@Column(name = ModelConstants.DEVICE_CUSTOMER_IS_PUBLIC_PROPERTY)
private boolean customerIsPublic; private boolean customerIsPublic;
@Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_NAME_PROPERTY)
private String deviceProfileName; private String deviceProfileName;
@Column(name = ModelConstants.DEVICE_ACTIVE_PROPERTY)
private boolean active;
public DeviceInfoEntity() { public DeviceInfoEntity() {
super(); super();
} }
public DeviceInfoEntity(DeviceEntity deviceEntity,
String customerTitle,
Object customerAdditionalInfo,
String deviceProfileName) {
super(deviceEntity);
this.customerTitle = customerTitle;
if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) {
this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean();
} else {
this.customerIsPublic = false;
}
this.deviceProfileName = deviceProfileName;
}
@Override @Override
public DeviceInfo toData() { public DeviceInfo toData() {
return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName); return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName, active);
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -109,7 +109,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.newAlarm, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.newAlarm, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.alarmUpdate, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmUpdate, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.deviceAction, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.entityAction, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.deviceActivity, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.deviceActivity, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId());

8
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -179,13 +179,13 @@ public class DefaultNotifications {
.description("Send notification to tenant admins when any alarm is updated or cleared") .description("Send notification to tenant admins when any alarm is updated or cleared")
.build()) .build())
.build(); .build();
public static final DefaultNotification deviceAction = DefaultNotification.builder() public static final DefaultNotification entityAction = DefaultNotification.builder()
.name("Device action notification") .name("Entity action notification")
.type(NotificationType.ENTITY_ACTION) .type(NotificationType.ENTITY_ACTION)
.subject("${entityType} was ${actionType}") .subject("${entityType} was ${actionType}")
.text("${entityType} '${entityName}' was ${actionType} by user ${userEmail}") .text("${entityType} '${entityName}' was ${actionType} by user ${userEmail}")
.icon("info").color(null) .icon("info").color(null)
.button("Go to device").link("/devices/${entityId}") .button("Go to ${entityType:lowerCase}").link("/${entityType:lowerCase}s/${entityId}")
.rule(DefaultRule.builder() .rule(DefaultRule.builder()
.name("Device created") .name("Device created")
.triggerConfig(EntityActionNotificationRuleTriggerConfig.builder() .triggerConfig(EntityActionNotificationRuleTriggerConfig.builder()
@ -341,7 +341,7 @@ public class DefaultNotifications {
public NotificationRule toRule(NotificationTemplateId templateId, NotificationTargetId... targets) { public NotificationRule toRule(NotificationTemplateId templateId, NotificationTargetId... targets) {
DefaultRule defaultRule = this.rule; DefaultRule defaultRule = this.rule;
NotificationRule rule = new NotificationRule(); NotificationRule rule = new NotificationRule();
rule.setName(name); rule.setName(defaultRule.getName());
rule.setTemplateId(templateId); rule.setTemplateId(templateId);
rule.setTriggerType(defaultRule.getTriggerConfig().getTriggerType()); rule.setTriggerType(defaultRule.getTriggerConfig().getTriggerType());
rule.setTriggerConfig(defaultRule.getTriggerConfig()); rule.setTriggerConfig(defaultRule.getTriggerConfig());

28
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -137,10 +137,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink()); log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
EntityId affectedEntity = query.getAffectedEntityId(); EntityId affectedEntity = query.getAffectedEntityId();
AlarmStatusFilter asf = AlarmStatusFilter.from(query); AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) { if (affectedEntity != null) {
return DaoUtil.toPageData( return DaoUtil.toPageData(
alarmRepository.findAlarms( alarmRepository.findAlarms(
@ -153,7 +149,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )
@ -168,7 +164,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )
@ -180,10 +176,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink()); log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink());
AlarmStatusFilter asf = AlarmStatusFilter.from(query); AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData( return DaoUtil.toPageData(
alarmRepository.findCustomerAlarms( alarmRepository.findCustomerAlarms(
tenantId.getId(), tenantId.getId(),
@ -194,7 +186,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )
@ -208,10 +200,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null; List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null; List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList()); AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) { if (affectedEntity != null) {
return DaoUtil.toPageData( return DaoUtil.toPageData(
alarmRepository.findAlarmsV2( alarmRepository.findAlarmsV2(
@ -226,7 +214,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )
@ -243,7 +231,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )
@ -257,10 +245,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null; List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null; List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList()); AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData( return DaoUtil.toPageData(
alarmRepository.findCustomerAlarmsV2( alarmRepository.findCustomerAlarmsV2(
tenantId.getId(), tenantId.getId(),
@ -273,7 +257,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(), asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(), asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(), asf.hasAckFilter() && asf.getAckFilter(),
assigneeId, DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""), Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink()) DaoUtil.toPageable(query.getPageLink())
) )

115
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.model.sql.DeviceEntity;
import org.thingsboard.server.dao.model.sql.DeviceInfoEntity; import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
@ -29,16 +28,9 @@ import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
*/
public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, ExportableEntityRepository<DeviceEntity> { public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, ExportableEntityRepository<DeviceEntity> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + @Query("SELECT d FROM DeviceInfoEntity d WHERE d.id = :deviceId")
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.id = :deviceId")
DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId); DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
@ -57,10 +49,7 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + @Query("SELECT d FROM DeviceInfoEntity d " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " + "WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " + "AND d.customerId = :customerId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))")
@ -79,19 +68,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(p.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.type = :type " + "AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
@ -105,9 +81,9 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
"AND d.firmwareId = null " + "AND d.firmwareId = null " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceEntity> findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId, Page<DeviceEntity> findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId, @Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.deviceProfileId = :deviceProfileId " + "AND d.deviceProfileId = :deviceProfileId " +
@ -130,34 +106,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
Long countByTenantIdAndDeviceProfileIdAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId, Long countByTenantIdAndDeviceProfileIdAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId); @Param("deviceProfileId") UUID deviceProfileId);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND d.type = :type " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND d.deviceProfileId = :deviceProfileId " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " + "AND d.customerId = :customerId " +
"AND d.type = :type " + "AND d.type = :type " +
@ -168,33 +116,26 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
Pageable pageable); Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + @Query("SELECT d FROM DeviceInfoEntity d " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " + "WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " + "AND (:customerId IS NULL OR d.customerId = uuid(:customerId)) " +
"AND d.type = :type " + "AND (:edgeId IS NULL OR d.id IN (SELECT re.toId FROM RelationEntity re WHERE re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' AND re.relationType = 'Contains' AND re.fromType = 'EDGE' AND re.fromId = uuid(:edgeId))) " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") "AND ((:deviceType) IS NULL OR d.type = :deviceType) " +
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, "AND (:deviceProfileId IS NULL OR d.deviceProfileId = uuid(:deviceProfileId)) " +
@Param("customerId") UUID customerId, "AND ((:filterByActive) IS FALSE OR d.active = :deviceActive) " +
@Param("type") String type, "AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
@Param("textSearch") String textSearch, "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
Pageable pageable); "OR LOWER(d.type) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.customerTitle) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " + Page<DeviceInfoEntity> findDeviceInfosByFilter(@Param("tenantId") UUID tenantId,
"FROM DeviceEntity d " + @Param("customerId") String customerId,
"LEFT JOIN CustomerEntity c on c.id = d.customerId " + @Param("edgeId") String edgeId,
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + @Param("deviceType") String type,
"WHERE d.tenantId = :tenantId " + @Param("deviceProfileId") String deviceProfileId,
"AND d.customerId = :customerId " + @Param("filterByActive") boolean filterByActive,
"AND d.deviceProfileId = :deviceProfileId " + @Param("deviceActive") boolean active,
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") @Param("textSearch") String textSearch,
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, Pageable pageable);
@Param("customerId") UUID customerId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId") @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId); List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId);
@ -234,13 +175,13 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
/** /**
* Count devices by tenantId. * Count devices by tenantId.
* Custom query applied because default QueryDSL produces slow count(id). * Custom query applied because default QueryDSL produces slow count(id).
* <p> *
* There is two way to count devices. * There is two way to count devices.
* OPTIMAL: count(*) * OPTIMAL: count(*)
* - returns _row_count_ and use index-only scan (super fast). * - returns _row_count_ and use index-only scan (super fast).
* SLOW: count(id) * SLOW: count(id)
* - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows. * - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows.
* */ */
@Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId") @Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId")
Long countByTenantId(@Param("tenantId") UUID tenantId); Long countByTenantId(@Param("tenantId") UUID tenantId);

72
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -16,8 +16,11 @@
package org.thingsboard.server.dao.sql.device; package org.thingsboard.server.dao.sql.device;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@ -26,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -43,9 +47,11 @@ import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -104,12 +110,18 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
} }
@Override @Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) { public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
return DaoUtil.toPageData( return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantId( deviceRepository.findDeviceInfosByFilter(
tenantId, filter.getTenantId().getId(),
DaoUtil.getStringId(filter.getCustomerId()),
DaoUtil.getStringId(filter.getEdgeId()),
filter.getType(),
DaoUtil.getStringId(filter.getDeviceProfileId()),
filter.getActive() != null,
Boolean.TRUE.equals(filter.getActive()),
Objects.toString(pageLink.getTextSearch(), ""), Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); DaoUtil.toPageable(pageLink)));
} }
@Override @Override
@ -152,16 +164,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
return DaoUtil.pageToPageData(deviceRepository.findIdsByDeviceProfileTransportType(transportType, DaoUtil.toPageable(pageLink))); return DaoUtil.pageToPageData(deviceRepository.findIdsByDeviceProfileTransportType(transportType, DaoUtil.toPageable(pageLink)));
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerId(
tenantId,
customerId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override @Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) { public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
return service.submit(() -> DaoUtil.convertDataList( return service.submit(() -> DaoUtil.convertDataList(
@ -208,26 +210,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
); );
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndType(
tenantId,
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndDeviceProfileId(
tenantId,
deviceProfileId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override @Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData( return DaoUtil.toPageData(
@ -239,28 +221,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink))); DaoUtil.toPageable(pageLink)));
} }
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndType(
tenantId,
customerId,
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(
tenantId,
customerId,
deviceProfileId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override @Override
public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) { public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId))); return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId)));

25
dao/src/main/resources/sql/schema-entities.sql

@ -859,6 +859,31 @@ CREATE TABLE IF NOT EXISTS user_settings (
CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type)
); );
DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_attribute_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(da.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active';
DROP VIEW IF EXISTS device_info_active_ts_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_ts_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(dt.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from ts_kv_dictionary where key = 'active');
DROP VIEW IF EXISTS device_info_view CASCADE;
CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;
DROP VIEW IF EXISTS alarm_info CASCADE; DROP VIEW IF EXISTS alarm_info CASCADE;
CREATE VIEW alarm_info AS CREATE VIEW alarm_info AS
SELECT a.*, SELECT a.*,

33
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
@ -446,7 +447,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name); device.setName(name);
device.setType("default"); device.setType("default");
devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default", false));
} }
String title2 = "Device title 2"; String title2 = "Device title 2";
List<DeviceInfo> devicesTitle2 = new ArrayList<>(); List<DeviceInfo> devicesTitle2 = new ArrayList<>();
@ -458,14 +459,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name); device.setName(name);
device.setType("default"); device.setType("default");
devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default", false));
} }
List<DeviceInfo> loadedDevicesTitle1 = new ArrayList<>(); List<DeviceInfo> loadedDevicesTitle1 = new ArrayList<>();
PageLink pageLink = new PageLink(15, 0, title1); PageLink pageLink = new PageLink(15, 0, title1);
PageData<DeviceInfo> pageData = null; PageData<DeviceInfo> pageData = null;
do { do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle1.addAll(pageData.getData()); loadedDevicesTitle1.addAll(pageData.getData());
if (pageData.hasNext()) { if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink(); pageLink = pageLink.nextPageLink();
@ -480,7 +481,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>(); List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>();
pageLink = new PageLink(4, 0, title2); pageLink = new PageLink(4, 0, title2);
do { do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle2.addAll(pageData.getData()); loadedDevicesTitle2.addAll(pageData.getData());
if (pageData.hasNext()) { if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink(); pageLink = pageLink.nextPageLink();
@ -497,7 +498,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
} }
pageLink = new PageLink(4, 0, title1); pageLink = new PageLink(4, 0, title1);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size()); Assert.assertEquals(0, pageData.getData().size());
@ -506,7 +507,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
} }
pageLink = new PageLink(4, 0, title2); pageLink = new PageLink(4, 0, title2);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size()); Assert.assertEquals(0, pageData.getData().size());
} }
@ -605,14 +606,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
device.setName("Device" + i); device.setName("Device" + i);
device.setType("default"); device.setType("default");
device = deviceService.saveDevice(device); device = deviceService.saveDevice(device);
devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default", false));
} }
List<DeviceInfo> loadedDevices = new ArrayList<>(); List<DeviceInfo> loadedDevices = new ArrayList<>();
PageLink pageLink = new PageLink(23); PageLink pageLink = new PageLink(23);
PageData<DeviceInfo> pageData = null; PageData<DeviceInfo> pageData = null;
do { do {
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId(customerId).build(), pageLink);
loadedDevices.addAll(pageData.getData()); loadedDevices.addAll(pageData.getData());
if (pageData.hasNext()) { if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink(); pageLink = pageLink.nextPageLink();
@ -627,7 +628,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
deviceService.unassignCustomerDevices(tenantId, customerId); deviceService.unassignCustomerDevices(tenantId, customerId);
pageLink = new PageLink(33); pageLink = new PageLink(33);
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId(customerId).build(), pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty()); Assert.assertTrue(pageData.getData().isEmpty());
} }
@ -848,7 +849,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithLabel).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty()); Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -862,7 +863,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithCustomer).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty()); Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -877,7 +878,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithType = new PageLink(100, 0, device.getType()); PageLink pageLinkWithType = new PageLink(100, 0, device.getType());
List<DeviceInfo> deviceInfosWithType = deviceService List<DeviceInfo> deviceInfosWithType = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithType).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithType).getData();
Assert.assertFalse(deviceInfosWithType.isEmpty()); Assert.assertFalse(deviceInfosWithType.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -907,7 +908,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantIdAndType(tenantId, device.getType(), pageLinkWithLabel).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).type(device.getType()).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty()); Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -922,7 +923,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantIdAndType(tenantId, device.getType(), pageLinkWithCustomer).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).type(device.getType()).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty()); Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -953,7 +954,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, savedDevice.getDeviceProfileId(), pageLinkWithLabel).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).deviceProfileId(savedDevice.getDeviceProfileId()).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty()); Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue( Assert.assertTrue(
@ -968,7 +969,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, savedDevice.getDeviceProfileId(), pageLinkWithCustomer).getData(); .findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).deviceProfileId(savedDevice.getDeviceProfileId()).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty()); Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue( Assert.assertTrue(

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

@ -32,8 +32,6 @@ public interface NotificationCenter {
void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId); void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId);
void sendBasicNotification(TenantId tenantId, UserId recipientId, String subject, String text);
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId); void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId);

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

5
ui-ngx/src/app/core/auth/auth.models.ts

@ -23,9 +23,14 @@ export interface SysParamsState {
edgesSupportEnabled: boolean; edgesSupportEnabled: boolean;
hasRepository: boolean; hasRepository: boolean;
tbelEnabled: boolean; tbelEnabled: boolean;
persistDeviceStateToTelemetry: boolean;
userSettings: UserSettings; userSettings: UserSettings;
} }
export interface SysParams extends SysParamsState {
maxDatapointsLimit: number;
}
export interface AuthPayload extends SysParamsState { export interface AuthPayload extends SysParamsState {
authUser: AuthUser; authUser: AuthUser;
userDetails: User; userDetails: User;

1
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -27,6 +27,7 @@ const emptyUserAuthState: AuthPayload = {
edgesSupportEnabled: false, edgesSupportEnabled: false,
hasRepository: false, hasRepository: false,
tbelEnabled: false, tbelEnabled: false,
persistDeviceStateToTelemetry: false,
userSettings: initialUserSettings userSettings: initialUserSettings
}; };

5
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -66,6 +66,11 @@ export const selectTbelEnabled = createSelector(
(state: AuthState) => state.tbelEnabled (state: AuthState) => state.tbelEnabled
); );
export const selectPersistDeviceStateToTelemetry = createSelector(
selectAuthState,
(state: AuthState) => state.persistDeviceStateToTelemetry
);
export const selectUserSettings = createSelector( export const selectUserSettings = createSelector(
selectAuthState, selectAuthState,
(state: AuthState) => state.userSettings (state: AuthState) => state.userSettings

98
ui-ngx/src/app/core/auth/auth.service.ts

@ -18,7 +18,7 @@ import { Injectable, NgZone } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt'; import { JwtHelperService } from '@auth0/angular-jwt';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; import { Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
@ -31,25 +31,17 @@ import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated
import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions';
import { AuthPayload, AuthState, SysParamsState } from '@core/auth/auth.models'; import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DashboardService } from '@core/http/dashboard.service';
import { PageLink } from '@shared/models/page/page-link';
import { DashboardInfo } from '@shared/models/dashboard.models';
import { PageData } from '@app/shared/models/page/page-data';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
import { isMobileApp } from '@core/utils'; import { isMobileApp } from '@core/utils';
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
import { UserPasswordPolicy } from '@shared/models/settings.models'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { UserSettings } from '@shared/models/user-settings.models';
import { UserSettingsService } from '@core/http/user-settings.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -65,10 +57,7 @@ export class AuthService {
private route: ActivatedRoute, private route: ActivatedRoute,
private zone: NgZone, private zone: NgZone,
private utils: UtilsService, private utils: UtilsService,
private dashboardService: DashboardService,
private adminService: AdminService,
private translate: TranslateService, private translate: TranslateService,
private userSettingsService: UserSettingsService,
private dialog: MatDialog private dialog: MatDialog
) { ) {
} }
@ -400,7 +389,7 @@ export class AuthService {
authPayload.forceFullscreen = true; authPayload.forceFullscreen = true;
} }
if (authPayload.authUser.isPublic) { if (authPayload.authUser.isPublic) {
this.loadSystemParams(authPayload).subscribe( this.loadSystemParams().subscribe(
(sysParams) => { (sysParams) => {
authPayload = {...authPayload, ...sysParams}; authPayload = {...authPayload, ...sysParams};
loadUserSubject.next(authPayload); loadUserSubject.next(authPayload);
@ -421,7 +410,7 @@ export class AuthService {
if (this.userForceFullscreen(authPayload)) { if (this.userForceFullscreen(authPayload)) {
authPayload.forceFullscreen = true; authPayload.forceFullscreen = true;
} }
this.loadSystemParams(authPayload).subscribe( this.loadSystemParams().subscribe(
(sysParams) => { (sysParams) => {
authPayload = {...authPayload, ...sysParams}; authPayload = {...authPayload, ...sysParams};
let userLang; let userLang;
@ -455,57 +444,14 @@ export class AuthService {
return loadUserSubject; return loadUserSubject;
} }
private loadIsUserTokenAccessEnabled(authUser: AuthUser): Observable<boolean> { private loadSystemParams(): Observable<SysParamsState> {
if (authUser.authority === Authority.SYS_ADMIN || return this.http.get<SysParams>('/api/system/params', defaultHttpOptions()).pipe(
authUser.authority === Authority.TENANT_ADMIN) { map((sysParams) => {
return this.http.get<boolean>('/api/user/tokenAccessEnabled', defaultHttpOptions()); this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit);
} else { return sysParams;
return of(false); }),
} catchError(() => of({} as SysParamsState))
} );
public loadIsEdgesSupportEnabled(): Observable<boolean> {
return this.http.get<boolean>('/api/edges/enabled', defaultHttpOptions());
}
private loadHasRepository(authUser: AuthUser): Observable<boolean> {
if (authUser.authority === Authority.TENANT_ADMIN) {
return this.http.get<boolean>('/api/admin/repositorySettings/exists', defaultHttpOptions());
} else {
return of(false);
}
}
private loadTbelEnabled(authUser: AuthUser): Observable<boolean> {
if (authUser.authority === Authority.TENANT_ADMIN) {
return this.http.get<boolean>('/api/ruleChain/tbelEnabled', defaultHttpOptions());
} else {
return of(false);
}
}
private loadUserSettings(): Observable<UserSettings> {
return this.userSettingsService.loadUserSettings();
}
private loadSystemParams(authPayload: AuthPayload): Observable<SysParamsState> {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(),
this.loadHasRepository(authPayload.authUser),
this.loadTbelEnabled(authPayload.authUser),
this.loadUserSettings(),
this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources)
.pipe(map((data) => {
const userTokenAccessEnabled: boolean = data[0] as boolean;
const allowedDashboardIds: string[] = data[1] as string[];
const edgesSupportEnabled: boolean = data[2] as boolean;
const hasRepository: boolean = data[3] as boolean;
const tbelEnabled: boolean = data[4] as boolean;
const userSettings = data[5] as UserSettings;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository, tbelEnabled, userSettings};
}, catchError((err) => of({}))));
} }
public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable<LoginResponse> { public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable<LoginResponse> {
@ -689,24 +635,4 @@ export class AuthService {
} }
} }
private fetchAllowedDashboardIds(authPayload: AuthPayload): Observable<string[]> {
if (authPayload.forceFullscreen && (authPayload.authUser.authority === Authority.TENANT_ADMIN ||
authPayload.authUser.authority === Authority.CUSTOMER_USER)) {
const pageLink = new PageLink(100);
let fetchDashboardsObservable: Observable<PageData<DashboardInfo>>;
if (authPayload.authUser.authority === Authority.TENANT_ADMIN) {
fetchDashboardsObservable = this.dashboardService.getTenantDashboards(pageLink);
} else {
fetchDashboardsObservable = this.dashboardService.getCustomerDashboards(authPayload.authUser.customerId, pageLink);
}
return fetchDashboardsObservable.pipe(
map((result) => {
const dashboards = result.data;
return dashboards.map(dashboard => dashboard.id.id);
})
);
} else {
return of([]);
}
}
} }

7
ui-ngx/src/app/core/http/device.service.ts

@ -25,7 +25,7 @@ import {
ClaimResult, ClaimResult,
Device, Device,
DeviceCredentials, DeviceCredentials,
DeviceInfo, DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery DeviceSearchQuery
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -42,6 +42,11 @@ export class DeviceService {
private http: HttpClient private http: HttpClient
) { } ) { }
public getDeviceInfosByQuery(deviceInfoQuery: DeviceInfoQuery, config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api${deviceInfoQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getTenantDeviceInfos(pageLink: PageLink, type: string = '', public getTenantDeviceInfos(pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<DeviceInfo>> { config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`, return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`,

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

@ -361,7 +361,8 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'mdi:alert-outline',
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',
@ -813,7 +814,8 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'mdi:alert-outline',
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',

16
ui-ngx/src/app/core/services/time.service.ts

@ -53,17 +53,11 @@ export class TimeService {
private http: HttpClient private http: HttpClient
) {} ) {}
public loadMaxDatapointsLimit(): Observable<number> { public setMaxDatapointsLimit(limit: number) {
return this.http.get<number>('/api/dashboard/maxDatapointsLimit', this.maxDatapointsLimit = limit;
defaultHttpOptions(true)).pipe( if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) {
map((limit) => { this.maxDatapointsLimit = MIN_LIMIT + 1;
this.maxDatapointsLimit = limit; }
if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) {
this.maxDatapointsLimit = MIN_LIMIT + 1;
}
return this.maxDatapointsLimit;
})
);
} }
public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean { public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean {

2
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts

@ -190,7 +190,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal
$event.stopPropagation(); $event.stopPropagation();
} }
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel', panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true, hasBackdrop: true,
maxHeight: '80vh', maxHeight: '80vh',

34
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -160,17 +160,41 @@
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'attribute.key' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'attribute.key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute" class="attribute">
{{ attribute.key }} <div fxLayout="row" style="align-items: center">
<span class="ellipsis">{{ attribute.key }}</span>
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.key"
tooltipText="{{ 'attribute.copy-key' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'attribute.value' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'attribute.value' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute" <mat-cell *matCellDef="let attribute"
class="tb-value-cell" class="tb-value-cell attribute"
(click)="editAttribute($event, attribute)"> (click)="editAttribute($event, attribute)">
<div fxLayout="row" style="align-items: center"> <div fxFlex fxLayout="row" style="align-items: center">
<span style="overflow: hidden;text-overflow: ellipsis;" fxFlex>{{attribute.value | tbJson}}</span> <div fxFlex fxLayout="row" style="overflow: hidden; align-items: center;">
<span class="ellipsis">{{attribute.value | tbJson}}</span>
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson"
tooltipText="{{ 'attribute.copy-value' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"> <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</span> </span>

17
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -51,6 +51,7 @@
overflow: auto; overflow: auto;
.mat-mdc-table { .mat-mdc-table {
table-layout: fixed; table-layout: fixed;
min-width: 450px;
} }
} }
@ -65,6 +66,22 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
.attribute-copy {
visibility: hidden;
transition: visibility 0.1s;
}
.attribute:hover {
.attribute-copy {
visibility: visible;
}
}
} }
} }

3
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -466,7 +466,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.singlePageMode = data.singlePageMode; this.singlePageMode = data.singlePageMode;
this.readonly = this.embedded || (this.singlePageMode && !this.widgetEditMode && !this.route.snapshot.queryParamMap.get('edit')) this.readonly = this.embedded || (this.singlePageMode && !this.widgetEditMode && !this.route.snapshot.queryParamMap.get('edit'))
|| this.forceFullscreen || this.isMobileApp || this.authUser.authority === Authority.CUSTOMER_USER; || this.forceFullscreen || this.isMobileApp || this.authUser.authority === Authority.CUSTOMER_USER ||
this.route.snapshot.queryParamMap.get('readonly') === 'true';
this.dashboardCtx.aliasController = this.parentAliasController ? this.parentAliasController : new AliasController(this.utils, this.dashboardCtx.aliasController = this.parentAliasController ? this.parentAliasController : new AliasController(this.utils,
this.entityService, this.entityService,

74
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html

@ -0,0 +1,74 @@
<!--
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="deviceFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else deviceFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleDeviceFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #deviceFilterPanel>
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="deviceFilter"></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]="deviceInfoFilterForm.invalid || !deviceInfoFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #deviceFilter>
<div fxLayout="column" fxLayoutGap="16px" style="min-width: 280px;" [formGroup]="deviceInfoFilterForm">
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
formControlName="deviceProfileId"
[displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<mat-form-field>
<mat-label translate>device.device-state</mat-label>
<mat-select formControlName="active">
<mat-option value="">
{{ 'device.any' | translate }}
</mat-option>
<mat-option [value]="true">
{{ 'device.active' | translate }}
</mat-option>
<mat-option [value]="false">
{{ 'device.inactive' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>

35
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: block;
overflow: hidden;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

254
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts

@ -0,0 +1,254 @@
///
/// 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,
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 { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { DeviceInfoFilter } from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
import { EntityInfoData } from '@shared/models/entity.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
export const DEVICE_FILTER_CONFIG_DATA = new InjectionToken<any>('DeviceFilterConfigData');
export interface DeviceFilterConfigData {
panelMode: boolean;
deviceInfoFilter: DeviceInfoFilter;
}
// @dynamic
@Component({
selector: 'tb-device-info-filter',
templateUrl: './device-info-filter.component.html',
styleUrls: ['./device-info-filter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoFilterComponent),
multi: true
}
]
})
export class DeviceInfoFilterComponent implements OnInit, OnDestroy, ControlValueAccessor {
@ViewChild('deviceFilterPanel')
deviceFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
panelMode = false;
buttonDisplayValue = this.translate.instant('device.device-filter');
deviceInfoFilterForm: UntypedFormGroup;
deviceFilterOverlayRef: OverlayRef;
panelResult: DeviceInfoFilter = null;
private deviceProfileInfo: EntityInfoData;
private deviceInfoFilter: DeviceInfoFilter;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(DEVICE_FILTER_CONFIG_DATA)
private data: DeviceFilterConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private deviceProfileService: DeviceProfileService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.deviceInfoFilter = this.data.deviceInfoFilter;
}
this.deviceInfoFilterForm = this.fb.group({
deviceProfileId: [null, []],
active: ['', []]
});
this.deviceInfoFilterForm.valueChanges.subscribe(
() => {
this.updateValidators();
if (!this.buttonMode) {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
}
}
);
if (this.panelMode) {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
}
}
ngOnDestroy(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceInfoFilterForm.disable({emitEvent: false});
} else {
this.deviceInfoFilterForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(deviceInfoFilter?: DeviceInfoFilter): void {
this.deviceInfoFilter = deviceInfoFilter;
this.updateButtonDisplayValue();
this.updateDeviceInfoFilterForm(deviceInfoFilter);
}
private updateValidators() {
}
toggleDeviceFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-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.deviceFilterOverlayRef = this.overlay.create(config);
this.deviceFilterOverlayRef.backdropClick().subscribe(() => {
this.deviceFilterOverlayRef.dispose();
});
this.deviceFilterOverlayRef.attach(new TemplatePortal(this.deviceFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
update() {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
if (this.panelMode) {
this.panelResult = this.deviceInfoFilter;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
deviceProfileChanged(deviceProfileInfo: EntityInfoData) {
this.deviceProfileInfo = deviceProfileInfo;
this.updateButtonDisplayValue();
}
private updateDeviceInfoFilterForm(deviceInfoFilter?: DeviceInfoFilter) {
this.deviceInfoFilterForm.patchValue({
deviceProfileId: deviceInfoFilter?.deviceProfileId,
active: isDefinedAndNotNull(deviceInfoFilter?.active) ? deviceInfoFilter?.active : ''
}, {emitEvent: false});
this.updateValidators();
}
private deviceFilterUpdated(deviceInfoFilter: DeviceInfoFilter) {
this.deviceInfoFilter = deviceInfoFilter;
if ((this.deviceInfoFilter.active as any) === '') {
this.deviceInfoFilter.active = null;
}
this.updateButtonDisplayValue();
this.propagateChange(this.deviceInfoFilter);
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (isDefinedAndNotNull(this.deviceInfoFilter?.deviceProfileId)) {
if (!this.deviceProfileInfo) {
this.deviceProfileService.getDeviceProfileInfo(this.deviceInfoFilter?.deviceProfileId.id,
{ignoreLoading: true, ignoreErrors: true}).subscribe(
(deviceProfileInfo) => {
this.deviceProfileChanged(deviceProfileInfo);
});
return;
} else {
filterTextParts.push(this.deviceProfileInfo.name);
}
}
if (isDefinedAndNotNull(this.deviceInfoFilter?.active)) {
const translationKey = this.deviceInfoFilter?.active ? 'device.active' : 'device.inactive';
filterTextParts.push(this.translate.instant(translationKey));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('device.device-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('device.filter-title') + `: ${filterTextParts.join(', ')}`;
}
this.cd.detectChanges();
}
}
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -181,6 +181,7 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -282,6 +283,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent, DeviceProfileComponent,
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
AssetProfileComponent, AssetProfileComponent,
AssetProfileDialogComponent, AssetProfileDialogComponent,
AssetProfileAutocompleteComponent, AssetProfileAutocompleteComponent,
@ -425,6 +427,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent, DeviceProfileComponent,
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
AssetProfileComponent, AssetProfileComponent,

4
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html

@ -27,6 +27,10 @@
(ngModelChange)="notifyActionUpdated()" (ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/> placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field> </mat-form-field>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
</mat-checkbox>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
type="button" type="button"

2
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw

@ -9,7 +9,6 @@
} }
.edit-entity-form .boolean-value-input .checkbox-label { .edit-entity-form .boolean-value-input .checkbox-label {
margin-bottom: 8px;
color: rgba(0,0,0,0.54); color: rgba(0,0,0,0.54);
font-size: 12px; font-size: 12px;
} }
@ -61,7 +60,6 @@
} }
.add-entity-form .boolean-value-input .checkbox-label { .add-entity-form .boolean-value-input .checkbox-label {
margin-bottom: 8px;
color: rgba(0,0,0,0.54); color: rgba(0,0,0,0.54);
font-size: 12px; font-size: 12px;
} }

2
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw

@ -111,7 +111,7 @@
<!-- <button mat-icon-button color="primary"--> <!-- <button mat-icon-button color="primary"-->
<!-- aria-label="Remove"--> <!-- aria-label="Remove"-->
<!-- type="button"--> <!-- type="button"-->
<!-- (click)="removeOldRelation(i)"--> <!-- (click)="removeOldRelation(i, relation.value)"-->
<!-- matTooltip="Remove relation"--> <!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">--> <!-- matTooltipPosition="above">-->
<!-- <mat-icon>close</mat-icon>--> <!-- <mat-icon>close</mat-icon>-->

4
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw

@ -136,9 +136,9 @@
// vm.relations().markAsDirty(); // vm.relations().markAsDirty();
// }; // };
// //
// vm.removeOldRelation = function(index) { // vm.removeOldRelation = function(index, relation) {
// vm.oldRelations().removeAt(index); // vm.oldRelations().removeAt(index);
// vm.relationsToDelete.push(vm.oldRelationsData[index]); // vm.relationsToDelete.push(relation);
// vm.oldRelations().markAsDirty(); // vm.oldRelations().markAsDirty();
// }; // };
// //

14
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -33,6 +33,8 @@ import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } fro
@Injectable() @Injectable()
export class CustomDialogService { export class CustomDialogService {
private customModules: Array<Type<any>>
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
private authService: AuthService, private authService: AuthService,
@ -44,12 +46,18 @@ export class CustomDialogService {
) { ) {
} }
setAdditionalModules(modules: Array<Type<any>>) {
this.customModules = modules;
}
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any,
config?: MatDialogConfig): Observable<any> { config?: MatDialogConfig): Observable<any> {
const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule];
if (Array.isArray(this.customModules)) {
modules.push(...this.customModules);
}
return this.dynamicComponentFactoryService.createDynamicComponentFactory( return this.dynamicComponentFactoryService.createDynamicComponentFactory(
class CustomDialogComponentInstance extends CustomDialogComponent {}, class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
template,
[this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]).pipe(
mergeMap((factory) => { mergeMap((factory) => {
const dialogData: CustomDialogContainerData = { const dialogData: CustomDialogContainerData = {
controller, controller,

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

@ -591,7 +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.panelClass = 'tb-filter-panel';
config.hasBackdrop = true; config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = { const connectedPosition: ConnectedPosition = {
originX: 'end', originX: 'end',

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

@ -67,4 +67,50 @@
color: inherit; color: inherit;
} }
} }
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
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;
}
}
} }

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

@ -16,91 +16,102 @@
--> -->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> <div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header"> <ng-container *ngIf="hasDashboardsAccess; else noDataAvailable">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a> <div class="tb-card-header">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px"> <a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[ <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.last'),
}, value: 'last'
{ },
name: ctx.translate.instant('widgets.recent-dashboards.starred'), {
value: 'starred' name: ctx.translate.instant('widgets.recent-dashboards.starred'),
} value: 'starred'
]"> }
</tb-toggle-header> ]">
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md </tb-toggle-header>
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a> <a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
</div> mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</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> </div>
<ng-template #noLastVisitedDashboards> </div>
<div class="tb-no-dashboards"> <ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<div class="tb-no-last-visited-bg"></div> <ng-template [ngSwitchCase]="'last'">
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div> <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> </div>
</ng-template> <ng-template #noLastVisitedDashboards>
</ng-template> <div class="tb-no-dashboards">
<ng-template [ngSwitchCase]="'starred'"> <div class="tb-no-last-visited-bg"></div>
<div style="overflow-y: auto;"> <div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
<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>
<div class="tb-cell title"> </ng-template>
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a> </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>
</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> </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-template>
</ng-container> </ng-container>
<ng-template #loading> <ng-template #noDataAvailable>
<div class="tb-no-dashboards"> <div class="tb-card-header">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner> <div class="tb-title">{{ 'widgets.recent-dashboards.title' | translate }}</div>
</div>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widgets.home.no-data-available</div>
</div> </div>
</ng-template> </ng-template>
</div> </div>

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

@ -74,6 +74,7 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
lastVisitedDashboardsPageLink: PageLink; lastVisitedDashboardsPageLink: PageLink;
starredDashboardValue = null; starredDashboardValue = null;
hasDashboardsAccess = true;
dirty = false; dirty = false;
@ -84,7 +85,10 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
} }
ngOnInit() { ngOnInit() {
this.reload(); this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) {
this.reload();
}
} }
reload() { reload() {

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

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

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

@ -44,6 +44,8 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
entityItemCritical: {[key: string]: boolean} = {}; entityItemCritical: {[key: string]: boolean} = {};
apiCallItemCritical: {[key: string]: boolean} = {}; apiCallItemCritical: {[key: string]: boolean} = {};
hasUsageInfoAccess = true;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe, private shortNumberPipe: ShortNumberPipe,
@ -52,41 +54,43 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
} }
ngOnInit() { ngOnInit() {
(this.authUser.authority === Authority.TENANT_ADMIN ? this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN;
this.usageInfoService.getUsageInfo() : of(null)).subscribe( if (this.hasUsageInfoAccess) {
(usageInfo) => { this.usageInfoService.getUsageInfo().subscribe(
this.usageInfo = usageInfo; (usageInfo) => {
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices); this.usageInfo = usageInfo;
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets); this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers); this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards); this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers); this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages, this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.usageInfo?.maxTransportMessages); this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions); this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms); this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails); this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms); this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
let entitiesHasCriticalItem = false; this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let apiCallsHasCriticalItem = false; let entitiesHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) { let apiCallsHasCriticalItem = false;
if (this.entityItemCritical[key]) { for (const key of Object.keys(this.entityItemCritical)) {
entitiesHasCriticalItem = true; if (this.entityItemCritical[key]) {
break; entitiesHasCriticalItem = true;
break;
}
} }
} for (const key of Object.keys(this.apiCallItemCritical)) {
for (const key of Object.keys(this.apiCallItemCritical)) { if (this.apiCallItemCritical[key]) {
if (this.apiCallItemCritical[key]) { apiCallsHasCriticalItem = true;
apiCallsHasCriticalItem = true; break;
break; }
} }
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
} }
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) { );
this.toggleValue = 'apiCalls'; }
}
this.cd.markForCheck();
}
);
} }
maxValue(max: number): number | string { maxValue(max: number): number | string {

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -23,7 +23,6 @@ import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-t
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';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module'; import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module';
import { import {
DateRangeNavigatorPanelComponent, DateRangeNavigatorPanelComponent,
@ -33,7 +32,6 @@ import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multip
import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component';
import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component';
import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
@ -93,8 +91,6 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
MarkdownWidgetComponent MarkdownWidgetComponent
], ],
providers: [ providers: [
CustomDialogService,
ImportExportService,
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }
] ]
}) })

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

@ -29,8 +29,10 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Optional,
Renderer2, Renderer2,
SimpleChanges, SimpleChanges,
Type,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
@ -89,7 +91,7 @@ import {
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { ResourcesService } from '@core/services/resources.service'; import { ModulesWithFactories, ResourcesService } from '@core/services/resources.service';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
@ -115,6 +117,8 @@ import { DialogService } from '@core/services/dialog.service';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens';
import { MODULES_MAP } from '@shared/models/constants';
import { IModulesMap } from '@modules/common/modules-map.models';
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -190,6 +194,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
@Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
@Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
private widgetService: WidgetService, private widgetService: WidgetService,
private resources: ResourcesService, private resources: ResourcesService,
private timeService: TimeService, private timeService: TimeService,
@ -1160,8 +1165,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (isDefined(customHtml) && customHtml.length > 0) { if (isDefined(customHtml) && customHtml.length > 0) {
htmlTemplate = customHtml; htmlTemplate = customHtml;
} }
this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({
() => { next: () => {
if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) {
try { try {
if (!additionalParams) { if (!additionalParams) {
@ -1169,16 +1174,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId',
'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction);
this.widgetContext.customDialog.setAdditionalModules(descriptor.customModules);
customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
}, },
(errorMessages: string[]) => { error: (errorMessages: string[]) => {
this.processResourcesLoadErrors(errorMessages); this.processResourcesLoadErrors(errorMessages);
} }
); });
break; break;
case WidgetActionType.mobileAction: case WidgetActionType.mobileAction:
const mobileAction = descriptor.mobileAction; const mobileAction = descriptor.mobileAction;
@ -1473,33 +1479,69 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
} }
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> { private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>, actionDescriptor: WidgetActionDescriptor): Observable<any> {
const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<ModulesWithFactories | string>[] = [];
if (isDefined(customCss) && customCss.length > 0) { if (isDefined(customCss) && customCss.length > 0) {
this.cssParser.cssPreviewNamespace = actionNamespace; this.cssParser.cssPreviewNamespace = actionNamespace;
this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace');
} }
const resourceTasks: Observable<string>[] = [];
if (isDefined(customResources) && customResources.length > 0) { if (isDefined(customResources) && customResources.length > 0) {
customResources.forEach((resource) => { customResources.forEach(resource => {
resourceTasks.push( if (resource.isModule) {
this.resources.loadResource(resource.url).pipe( modulesTasks.push(
catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) this.resources.loadFactories(resource.url, this.modulesMap).pipe(
) catchError((e: Error) => of(e?.message ? e.message : `Failed to load custom action resource module: '${resource.url}'`))
); )
);
} else {
resourceTasks.push(
this.resources.loadResource(resource.url).pipe(
catchError(() => of(`Failed to load custom action resource: '${resource.url}'`))
)
);
}
}); });
return forkJoin(resourceTasks).pipe(
switchMap(msgs => { if (modulesTasks.length) {
let errors: string[]; const modulesObservable: Observable<string | Type<any>[]> = forkJoin(modulesTasks).pipe(
if (msgs && msgs.length) { map(res => {
errors = msgs.filter(msg => msg && msg.length > 0); const msg = res.find(r => typeof r === 'string');
if (msg) {
return msg as string;
} else {
const modulesWithFactoriesList = res as ModulesWithFactories[];
const resModulesWithFactories: ModulesWithFactories = {
modules: modulesWithFactoriesList.map(mf => mf.modules).flat(),
factories: modulesWithFactoriesList.map(mf => mf.factories).flat()
};
return resModulesWithFactories.modules;
} }
if (errors && errors.length) { })
return throwError(errors); );
resourceTasks.push(modulesObservable.pipe(
map((resolvedModules) => {
if (typeof resolvedModules === 'string') {
return resolvedModules;
} else { } else {
return of(null); actionDescriptor.customModules = resolvedModules;
return null;
} }
} })));
)); }
return forkJoin(resourceTasks).pipe(
switchMap(msgs => {
const errors = msgs.filter(msg => msg && msg.length > 0);
if (errors.length > 0) {
return throwError(() => errors);
} else {
return of(null);
}}
));
} else { } else {
return of(null); return of(null);
} }

2
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -42,7 +42,7 @@ const routes: Routes = [
title: 'alarm.alarms', title: 'alarm.alarms',
breadcrumb: { breadcrumb: {
label: 'alarm.alarms', label: 'alarm.alarms',
icon: 'notifications' icon: 'mdi:alert-outline'
}, },
isPage: true, isPage: true,
alarmsMode: AlarmsMode.ALL alarmsMode: AlarmsMode.ALL

15
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts

@ -22,18 +22,22 @@ import { Authority } from '@shared/models/authority.enum';
import { DashboardsTableConfigResolver } from './dashboards-table-config.resolver'; import { DashboardsTableConfigResolver } from './dashboards-table-config.resolver';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { mergeMap, Observable } from 'rxjs'; import { mergeMap, Observable, of } from 'rxjs';
import { Dashboard } from '@app/shared/models/dashboard.models'; import { Dashboard } from '@app/shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { UserDashboardAction } from '@shared/models/user-settings.models'; import { UserDashboardAction } from '@shared/models/user-settings.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@Injectable() @Injectable()
export class DashboardResolver implements Resolve<Dashboard> { export class DashboardResolver implements Resolve<Dashboard> {
constructor(private dashboardService: DashboardService, constructor(private store: Store<AppState>,
private dashboardService: DashboardService,
private userSettingService: UserSettingsService, private userSettingService: UserSettingsService,
private dashboardUtils: DashboardUtilsService) { private dashboardUtils: DashboardUtilsService) {
} }
@ -42,7 +46,10 @@ export class DashboardResolver implements Resolve<Dashboard> {
const dashboardId = route.params.dashboardId; const dashboardId = route.params.dashboardId;
return this.dashboardService.getDashboard(dashboardId).pipe( return this.dashboardService.getDashboard(dashboardId).pipe(
mergeMap((dashboard) => mergeMap((dashboard) =>
this.userSettingService.reportUserDashboardAction(dashboardId, UserDashboardAction.VISIT, {ignoreLoading: true}).pipe( (getCurrentAuthUser(this.store).isPublic ? of(null) :
this.userSettingService.reportUserDashboardAction(dashboardId, UserDashboardAction.VISIT,
{ignoreLoading: true, ignoreErrors: true})).pipe(
catchError(() => of(dashboard)),
map(() => dashboard) map(() => dashboard)
)), )),
map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard)) map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard))

10
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html

@ -15,10 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<tb-device-profile-autocomplete <tb-device-info-filter [ngModel]="entitiesTableConfig.componentsData.deviceInfoFilter"
subscriptSizing="dynamic" (ngModelChange)="deviceInfoFilterChanged($event)">
[ngModel]="entitiesTableConfig.componentsData.deviceProfileId" </tb-device-info-filter>
(ngModelChange)="deviceProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>

6
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts

@ -18,7 +18,7 @@ import { Component } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { DeviceInfo } from '@app/shared/models/device.models'; import { DeviceInfo, DeviceInfoFilter } from '@app/shared/models/device.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id';
@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<Devic
super(store); super(store);
} }
deviceProfileChanged(deviceProfileId: DeviceProfileId) { deviceInfoFilterChanged(deviceInfoFilter: DeviceInfoFilter) {
this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId; this.entitiesTableConfig.componentsData.deviceInfoFilter = deviceInfoFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true); this.entitiesTableConfig.getTable().resetSortAndFilter(true);
} }

121
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { import {
CellActionDescriptor, CellActionDescriptor,
checkBoxCell, checkBoxCell,
@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models'; import {
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfoFilter,
DeviceInfoQuery
} from '@app/shared/models/device.models';
import { DeviceComponent } from '@modules/home/pages/device/device.component'; import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs'; import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
@ -63,12 +69,21 @@ import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component'; import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
import { EdgeService } from '@core/http/edge.service'; import { EdgeService } from '@core/http/edge.service';
import { import {
AddEntitiesToEdgeDialogComponent, AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData AddEntitiesToEdgeDialogData
} from '@home/dialogs/add-entities-to-edge-dialog.component'; } from '@home/dialogs/add-entities-to-edge-dialog.component';
import { EdgeId } from '@shared/models/id/edge-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
interface DevicePageQueryParams extends PageQueryParam {
deviceProfileId?: string;
active?: boolean | string;
}
@Injectable() @Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
@ -103,17 +118,16 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text'); this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id); this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => { this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe(
return this.deviceService.saveDevice(device).pipe(
tap(() => { tap(() => {
this.broadcast.broadcast('deviceSaved'); this.broadcast.broadcast('deviceSaved');
}), }),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id) mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
)); ));
};
this.config.onEntityAction = action => this.onDeviceAction(action, this.config); this.config.onEntityAction = action => this.onDeviceAction(action, this.config);
this.config.detailsReadonly = () => this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'); (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.onLoadAction = (route) => this.onLoadAction(route);
this.config.headerComponent = DeviceTableHeaderComponent; this.config.headerComponent = DeviceTableHeaderComponent;
@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
const routeParams = route.params; const routeParams = route.params;
this.config.componentsData = { this.config.componentsData = {
deviceScope: route.data.devicesType, deviceScope: route.data.devicesType,
deviceProfileId: null, deviceInfoFilter: {},
deviceCredentials$: new Subject<DeviceCredentials>(), deviceCredentials$: new Subject<DeviceCredentials>(),
edgeId: routeParams.edgeId edgeId: routeParams.edgeId
}; };
@ -162,7 +176,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'); this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config; return this.config;
@ -170,48 +185,97 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
); );
} }
onLoadAction(route: ActivatedRoute): void {
const routerQueryParams: DevicePageQueryParams = route.snapshot.queryParams;
if (routerQueryParams) {
const queryParams = deepClone(routerQueryParams);
let replaceUrl = false;
if (routerQueryParams?.deviceProfileId) {
this.config.componentsData.deviceInfoFilter.deviceProfileId = new DeviceProfileId(routerQueryParams?.deviceProfileId);
delete queryParams.deviceProfileId;
replaceUrl = true;
}
if (isDefined(routerQueryParams?.active)) {
this.config.componentsData.deviceInfoFilter.active = (routerQueryParams?.active === true || routerQueryParams?.active === 'true');
delete queryParams.active;
replaceUrl = true;
}
if (replaceUrl) {
this.router.navigate([], {
relativeTo: route,
queryParams,
queryParamsHandling: '',
replaceUrl: true
});
}
}
}
configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> { configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [ const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'), new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
new EntityTableColumn<DeviceInfo>('deviceProfileName', 'device-profile.device-profile', '25%'), new EntityTableColumn<DeviceInfo>('deviceProfileName', 'device-profile.device-profile', '25%'),
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%') new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%'),
new EntityTableColumn<DeviceInfo>('active', 'device.state', '80px',
entity => this.deviceState(entity), entity => this.deviceStateStyle(entity))
]; ];
if (deviceScope === 'tenant') { if (deviceScope === 'tenant') {
columns.push( columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'), new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px', new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => { entity => checkBoxCell(entity.customerIsPublic), () => ({})),
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
); );
} }
columns.push( columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px', new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => { entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false)
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
); );
return columns; return columns;
} }
private deviceState(device: DeviceInfo): string {
let translateKey = 'device.active';
let backgroundColor = 'rgba(25, 128, 56, 0.08)';
if (!device.active) {
translateKey = 'device.inactive';
backgroundColor = 'rgba(209, 39, 48, 0.08)';
}
return `<div class="status" style="border-radius: 16px; height: 32px;
line-height: 32px; padding: 0 12px; width: fit-content; background-color: ${backgroundColor}">
${this.translate.instant(translateKey)}
</div>`;
}
private deviceStateStyle(device: DeviceInfo): object {
const styleObj = {
fontSize: '14px',
color: '#198038',
cursor: 'pointer'
};
if (!device.active) {
styleObj.color = '#d12730';
}
return styleObj;
}
configureEntityFunctions(deviceScope: string): void { configureEntityFunctions(deviceScope: string): void {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink));
if (deviceScope === 'tenant') { if (deviceScope === 'tenant') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink,
this.config.componentsData.deviceProfileId !== null ?
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id);
} else if (deviceScope === 'edge' || deviceScope === 'edge_customer_user') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getEdgeDevices(this.config.componentsData.edgeId, pageLink, this.config.componentsData.edgeType);
} else { } else {
this.config.entitiesFetchFunction = pageLink => this.config.deleteEntity = () => of();
this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink, }
this.config.componentsData.deviceProfileId !== null ? }
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); prepareDeviceInfoQuery(pageLink: PageLink): DeviceInfoQuery {
const deviceInfoFilter: DeviceInfoFilter = deepClone(this.config.componentsData.deviceInfoFilter);
if (this.config.componentsData.deviceScope === 'edge' || this.config.componentsData.deviceScope === 'edge_customer_user') {
deviceInfoFilter.edgeId = new EdgeId(this.config.componentsData.edgeId);
} else if (this.config.componentsData.deviceScope !== 'tenant') {
deviceInfoFilter.customerId = new CustomerId(this.customerId);
} }
return new DeviceInfoQuery(pageLink, deviceInfoFilter);
} }
configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> { configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
@ -540,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
data: { data: {
deviceId: device.id.id, deviceId: device.id.id,
deviceProfileId: device.deviceProfileId.id, deviceProfileId: device.deviceProfileId.id,
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user' isReadOnly: this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user'
} }
}).afterClosed().subscribe(deviceCredentials => { }).afterClosed().subscribe(deviceCredentials => {
if (isDefinedAndNotNull(deviceCredentials)) { if (isDefinedAndNotNull(deviceCredentials)) {

2
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -260,7 +260,7 @@ const routes: Routes = [
path: ':edgeId/ruleChains', path: ':edgeId/ruleChains',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.edge-rulechains', label: 'edge.rulechains',
icon: 'settings_ethernet' icon: 'settings_ethernet'
} }
}, },

2
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -117,7 +117,7 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openInstructions')" (click)="onEntityAction($event, 'openInstructions')"
[fxShow]="!isEdit"> [fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon>info_outline</mat-icon> <mat-icon>info_outline</mat-icon>
<span>{{ 'edge.install-connect-instructions' | translate }}</span> <span>{{ 'edge.install-connect-instructions' | translate }}</span>
</button> </button>

18
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -211,31 +211,31 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
onAction: ($event, entity) => this.unassignFromCustomer($event, entity) onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
}, },
{ {
name: this.translate.instant('edge.edge-assets'), name: this.translate.instant('edge.manage-assets'),
icon: 'domain', icon: 'domain',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET)
}, },
{ {
name: this.translate.instant('edge.edge-devices'), name: this.translate.instant('edge.manage-devices'),
icon: 'devices_other', icon: 'devices_other',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE)
}, },
{ {
name: this.translate.instant('edge.edge-entity-views'), name: this.translate.instant('edge.manage-entity-views'),
icon: 'view_quilt', icon: 'view_quilt',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW)
}, },
{ {
name: this.translate.instant('edge.edge-dashboards'), name: this.translate.instant('edge.manage-dashboards'),
icon: 'dashboard', icon: 'dashboard',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD)
}, },
{ {
name: this.translate.instant('edge.edge-rulechains'), name: this.translate.instant('edge.manage-rulechains'),
icon: 'settings_ethernet', icon: 'settings_ethernet',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.RULE_CHAIN) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.RULE_CHAIN)
@ -261,25 +261,25 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
if (edgeScope === 'customer_user') { if (edgeScope === 'customer_user') {
actions.push( actions.push(
{ {
name: this.translate.instant('edge.edge-assets'), name: this.translate.instant('edge.manage-assets'),
icon: 'domain', icon: 'domain',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET)
}, },
{ {
name: this.translate.instant('edge.edge-devices'), name: this.translate.instant('edge.manage-devices'),
icon: 'devices_other', icon: 'devices_other',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE)
}, },
{ {
name: this.translate.instant('edge.edge-entity-views'), name: this.translate.instant('edge.manage-entity-views'),
icon: 'view_quilt', icon: 'view_quilt',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW)
}, },
{ {
name: this.translate.instant('edge.edge-dashboards'), name: this.translate.instant('edge.manage-dashboards'),
icon: 'dashboard', icon: 'dashboard',
isEnabled: (entity) => true, isEnabled: (entity) => true,
onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD)

44
ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts

@ -19,16 +19,21 @@ import { Resolve, RouterModule, Routes } from '@angular/router';
import { HomeLinksComponent } from './home-links.component'; import { HomeLinksComponent } from './home-links.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs'; import { mergeMap, Observable, of } from 'rxjs';
import { HomeDashboard } from '@shared/models/dashboard.models'; import { HomeDashboard } from '@shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import {
getCurrentAuthUser,
selectHasRepository,
selectPersistDeviceStateToTelemetry
} from '@core/auth/auth.selectors';
import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw'; import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw';
import tenantAdminHomePageDashboardJson from '!raw-loader!./tenant_admin_home_page.raw'; import tenantAdminHomePageDashboardJson from '!raw-loader!./tenant_admin_home_page.raw';
import customerUserHomePageDashboardJson from '!raw-loader!./customer_user_home_page.raw'; import customerUserHomePageDashboardJson from '!raw-loader!./customer_user_home_page.raw';
import { EntityKeyType } from '@shared/models/query/query.models';
@Injectable() @Injectable()
export class HomeDashboardResolver implements Resolve<HomeDashboard> { export class HomeDashboardResolver implements Resolve<HomeDashboard> {
@ -39,22 +44,43 @@ export class HomeDashboardResolver implements Resolve<HomeDashboard> {
resolve(): Observable<HomeDashboard> { resolve(): Observable<HomeDashboard> {
return this.dashboardService.getHomeDashboard().pipe( return this.dashboardService.getHomeDashboard().pipe(
map((dashboard) => { mergeMap((dashboard) => {
if (!dashboard) { if (!dashboard) {
let dashboard$: Observable<HomeDashboard>;
const authority = getCurrentAuthUser(this.store).authority; const authority = getCurrentAuthUser(this.store).authority;
switch (authority) { switch (authority) {
case Authority.SYS_ADMIN: case Authority.SYS_ADMIN:
dashboard = JSON.parse(sysAdminHomePageDashboardJson); dashboard$ = of(JSON.parse(sysAdminHomePageDashboardJson));
break; break;
case Authority.TENANT_ADMIN: case Authority.TENANT_ADMIN:
dashboard = JSON.parse(tenantAdminHomePageDashboardJson); dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(tenantAdminHomePageDashboardJson));
break; break;
case Authority.CUSTOMER_USER: case Authority.CUSTOMER_USER:
dashboard = JSON.parse(customerUserHomePageDashboardJson); dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(customerUserHomePageDashboardJson));
break; break;
} }
if (dashboard) { if (dashboard$) {
dashboard.hideDashboardToolbar = true; return dashboard$.pipe(
map((homeDashboard) => {
homeDashboard.hideDashboardToolbar = true;
return homeDashboard;
})
);
}
}
return of(dashboard);
})
);
}
private updateDeviceActivityKeyFilterIfNeeded(dashboard: HomeDashboard): Observable<HomeDashboard> {
return this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
map((persistToTelemetry) => {
if (persistToTelemetry) {
for (const filterId of Object.keys(dashboard.configuration.filters)) {
if (['Active Devices', 'Inactive Devices'].includes(dashboard.configuration.filters[filterId].filter)) {
dashboard.configuration.filters[filterId].keyFilters[0].key.type = EntityKeyType.TIME_SERIES;
}
} }
} }
return dashboard; return dashboard;

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

@ -648,9 +648,9 @@
"padding": "16px", "padding": "16px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>", "markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\" [queryParams]=\"{active: false}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\" [queryParams]=\"{active: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n" "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
}, },
"title": "Devices", "title": "Devices",
"showTitleIcon": false, "showTitleIcon": false,
@ -804,7 +804,7 @@
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\" [queryParams]=\"{severityList: ['CRITICAL']}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\" [queryParams]=\"{assignedToMe: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>", "markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\" [queryParams]=\"{severityList: ['CRITICAL']}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\" [queryParams]=\"{assignedToMe: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n" "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n"
}, },
"title": "Alarms", "title": "Alarms",
"showTitleIcon": false, "showTitleIcon": false,

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -198,7 +198,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
} else if (ruleChainScope === 'edges') { } else if (ruleChainScope === 'edges') {
return this.translate.instant('edge.rulechain-templates'); return this.translate.instant('edge.rulechain-templates');
} else if (ruleChainScope === 'edge') { } else if (ruleChainScope === 'edge') {
return this.config.tableTitle = edge.name + ': ' + this.translate.instant('edge.rulechains'); return this.config.tableTitle = edge.name + ': ' + this.translate.instant('rulechain.rulechains');
} }
} }

2
ui-ngx/src/app/shared/models/constants.ts

@ -146,7 +146,7 @@ export const HelpLinks = {
entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
resources: helpBaseUrl + '/docs/user-guide/ui/resources', lwm2mResourceLibrary: helpBaseUrl + '/docs/reference/lwm2m-api',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates', otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',

128
ui-ngx/src/app/shared/models/device.models.ts

@ -35,6 +35,9 @@ import {
getDefaultProfileObserveAttrConfig, getDefaultProfileObserveAttrConfig,
PowerMode PowerMode
} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { PageLink } from '@shared/models/page/page-link';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { EdgeId } from '@shared/models/id/edge-id';
export enum DeviceProfileType { export enum DeviceProfileType {
DEFAULT = 'DEFAULT', DEFAULT = 'DEFAULT',
@ -328,7 +331,7 @@ export interface DeviceProvisionConfiguration {
allowCreateNewDevicesByX509Certificate?: boolean; allowCreateNewDevicesByX509Certificate?: boolean;
} }
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { export const createDeviceProfileConfiguration = (type: DeviceProfileType): DeviceProfileConfiguration => {
let configuration: DeviceProfileConfiguration = null; let configuration: DeviceProfileConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -339,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic
} }
} }
return configuration; return configuration;
} };
export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => {
let configuration: DeviceConfiguration = null; let configuration: DeviceConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -352,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig
} }
} }
return configuration; return configuration;
} };
export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => {
let transportConfiguration: DeviceProfileTransportConfiguration = null; let transportConfiguration: DeviceProfileTransportConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -368,6 +371,7 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
deviceAttributesTopic: 'v1/devices/me/attributes', deviceAttributesTopic: 'v1/devices/me/attributes',
deviceAttributesSubscribeTopic: 'v1/devices/me/attributes', deviceAttributesSubscribeTopic: 'v1/devices/me/attributes',
sparkplug: false, sparkplug: false,
sparkplugAttributesMetricNames: ['Node Control/*', 'Device Control/*', 'Properties/*'],
sendAckOnValidationException: false, sendAckOnValidationException: false,
transportPayloadTypeConfiguration: { transportPayloadTypeConfiguration: {
transportPayloadType: TransportPayloadType.JSON, transportPayloadType: TransportPayloadType.JSON,
@ -408,9 +412,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
} }
} }
return transportConfiguration; return transportConfiguration;
} };
export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => {
let transportConfiguration: DeviceTransportConfiguration = null; let transportConfiguration: DeviceTransportConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -446,7 +450,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
} }
} }
return transportConfiguration; return transportConfiguration;
} };
export enum AlarmConditionType { export enum AlarmConditionType {
SIMPLE = 'SIMPLE', SIMPLE = 'SIMPLE',
@ -489,7 +493,7 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
export interface AlarmSchedule{ export interface AlarmSchedule{
dynamicValue?: { dynamicValue?: {
sourceAttribute: string, sourceAttribute: string;
sourceType: string; sourceType: string;
}; };
type: AlarmScheduleType; type: AlarmScheduleType;
@ -516,17 +520,13 @@ interface AlarmRule {
export { AlarmRule as DeviceProfileAlarmRule }; export { AlarmRule as DeviceProfileAlarmRule };
export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null { const alarmRuleValid = (alarmRule: AlarmRule): boolean =>
!(!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length);
export const alarmRuleValidator = (control: AbstractControl): ValidationErrors | null => {
const alarmRule: AlarmRule = control.value; const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; return alarmRuleValid(alarmRule) ? null : {alarmRule: true};
} };
function alarmRuleValid(alarmRule: AlarmRule): boolean {
if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) {
return false;
}
return true;
}
export interface DeviceProfileAlarm { export interface DeviceProfileAlarm {
id: string; id: string;
@ -539,7 +539,7 @@ export interface DeviceProfileAlarm {
propagateRelationTypes?: Array<string>; propagateRelationTypes?: Array<string>;
} }
export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null { export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => {
const deviceProfileAlarm: DeviceProfileAlarm = control.value; const deviceProfileAlarm: DeviceProfileAlarm = control.value;
if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType && if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType &&
deviceProfileAlarm.createRules) { deviceProfileAlarm.createRules) {
@ -564,7 +564,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio
} }
} }
return {deviceProfileAlarm: true}; return {deviceProfileAlarm: true};
} };
export interface DeviceProfileData { export interface DeviceProfileData {
@ -718,6 +718,47 @@ export interface DeviceInfo extends Device {
customerTitle: string; customerTitle: string;
customerIsPublic: boolean; customerIsPublic: boolean;
deviceProfileName: string; deviceProfileName: string;
active: boolean;
}
export interface DeviceInfoFilter {
customerId?: CustomerId;
edgeId?: EdgeId;
type?: string;
deviceProfileId?: DeviceProfileId;
active?: boolean;
}
export class DeviceInfoQuery {
pageLink: PageLink;
deviceInfoFilter: DeviceInfoFilter;
constructor(pageLink: PageLink, deviceInfoFilter: DeviceInfoFilter) {
this.pageLink = pageLink;
this.deviceInfoFilter = deviceInfoFilter;
}
public toQuery(): string {
let query;
if (this.deviceInfoFilter.customerId) {
query = `/customer/${this.deviceInfoFilter.customerId.id}/deviceInfos`;
} else if (this.deviceInfoFilter.edgeId) {
query = `/edge/${this.deviceInfoFilter.edgeId.id}/devices`;
} else {
query = '/tenant/deviceInfos';
}
query += this.pageLink.toQuery();
if (isNotEmptyStr(this.deviceInfoFilter.type)) {
query += `&type=${this.deviceInfoFilter.type}`;
} else if (this.deviceInfoFilter.deviceProfileId) {
query += `&deviceProfileId=${this.deviceInfoFilter.deviceProfileId.id}`;
}
if (isDefinedAndNotNull(this.deviceInfoFilter.active)) {
query += `&active=${this.deviceInfoFilter.active}`;
}
return query;
}
} }
export enum DeviceCredentialsType { export enum DeviceCredentialsType {
@ -763,13 +804,11 @@ export interface DeviceCredentialMQTTBasic {
password: string; password: string;
} }
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic { export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({
return { clientId: '',
clientId: '', userName: '',
userName: '', password: ''
password: '' });
};
}
export interface DeviceSearchQuery extends EntitySearchQuery { export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>; deviceTypes: Array<string>;
@ -800,44 +839,23 @@ export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.sunday' 'device-profile.schedule-day.sunday'
); );
export function getDayString(day: number): string { export const timeOfDayToUTCTimestamp = (date: Date | number): number => {
switch (day) {
case 0:
return 'device-profile.schedule-day.monday';
case 1:
return this.translate.instant('device-profile.schedule-day.tuesday');
case 2:
return this.translate.instant('device-profile.schedule-day.wednesday');
case 3:
return this.translate.instant('device-profile.schedule-day.thursday');
case 4:
return this.translate.instant('device-profile.schedule-day.friday');
case 5:
return this.translate.instant('device-profile.schedule-day.saturday');
case 6:
return this.translate.instant('device-profile.schedule-day.sunday');
}
}
export function timeOfDayToUTCTimestamp(date: Date | number): number {
if (typeof date === 'number' || date === null) { if (typeof date === 'number' || date === null) {
return 0; return 0;
} }
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
} };
export function utcTimestampToTimeOfDay(time = 0): Date { export const utcTimestampToTimeOfDay = (time = 0): Date => new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
}
function timeOfDayToMoment(date: Date | number): _moment.Moment { const timeOfDayToMoment = (date: Date | number): _moment.Moment => {
if (typeof date === 'number' || date === null) { if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]); return _moment([1970, 0, 1, 0, 0, 0, 0]);
} }
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
} };
export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string { export const getAlarmScheduleRangeText = (startsOn: Date | number, endsOn: Date | number): string => {
const start = timeOfDayToMoment(startsOn); const start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn); const end = timeOfDayToMoment(endsOn);
if (start < end) { if (start < end) {
@ -847,4 +865,4 @@ export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date
} }
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` + return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`; ` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
} };

2
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -477,7 +477,7 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
[ [
EntityType.TB_RESOURCE, EntityType.TB_RESOURCE,
{ {
helpLinkId: 'resources' helpLinkId: 'lwm2mResourceLibrary'
} }
], ],
[ [

3
ui-ngx/src/app/shared/models/widget.models.ts

@ -32,7 +32,7 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; import { AfterViewInit, Directive, EventEmitter, Inject, OnInit, Type } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
@ -560,6 +560,7 @@ export interface CustomActionDescriptor {
customResources?: Array<WidgetResource>; customResources?: Array<WidgetResource>;
customHtml?: string; customHtml?: string;
customCss?: string; customCss?: string;
customModules?: Type<any>[];
} }
export interface WidgetActionDescriptor extends CustomActionDescriptor { export interface WidgetActionDescriptor extends CustomActionDescriptor {

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md

@ -82,7 +82,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

10
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md

@ -5,7 +5,7 @@
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" <form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
(ngSubmit)="save()" class="edit-entity-form"> (ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2> <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button"> <button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
aria-label="Remove" aria-label="Remove"
type="button" type="button"
(click)="removeOldRelation(i)" (click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation" matTooltip="Remove relation"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md

@ -82,9 +82,9 @@ function EditEntityDialogController(instance) {
vm.relations().markAsDirty(); vm.relations().markAsDirty();
}; };
vm.removeOldRelation = function(index) { vm.removeOldRelation = function(index, relation) {
vm.oldRelations().removeAt(index); vm.oldRelations().removeAt(index);
vm.relationsToDelete.push(vm.oldRelationsData[index]); vm.relationsToDelete.push(relation);
vm.oldRelations().markAsDirty(); vm.oldRelations().markAsDirty();
}; };

4
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md

@ -82,7 +82,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

10
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md

@ -5,7 +5,7 @@
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" <form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
(ngSubmit)="save()" class="edit-entity-form"> (ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2> <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button"> <button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
aria-label="Remove" aria-label="Remove"
type="button" type="button"
(click)="removeOldRelation(i)" (click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation" matTooltip="Remove relation"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

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

Loading…
Cancel
Save