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.
--
-- 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
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_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_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 ASSET_TYPE_DESCRIPTION = "Asset type";
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.Device;
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.SaveDeviceWithCredentialsRequest;
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_ID;
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_PARAM_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,
@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)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -342,14 +346,15 @@ public class DeviceController extends BaseController {
) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
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) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink));
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Get Tenant Device (getTenantDevice)",
@ -415,6 +420,8 @@ public class DeviceController extends BaseController {
@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)
@RequestParam(required = false) String textSearch,
@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));
checkCustomerId(customerId, Operation.READ);
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) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
@ -671,7 +680,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Device> getEdgeDevices(
public PageData<DeviceInfo> getEdgeDevices(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@ -680,6 +689,10 @@ public class DeviceController extends BaseController {
@RequestParam int page,
@ApiParam(value = DEVICE_TYPE_DESCRIPTION)
@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)
@RequestParam(required = false) String textSearch,
@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));
checkEdgeId(edgeId, Operation.READ);
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) {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink);
} else {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
List<Device> filteredDevices = nonFilteredResult.getData().stream().filter(device -> {
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);
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@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;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
@ -76,6 +77,16 @@ import java.util.Set;
import java.util.UUID;
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;
@RestController
@ -93,7 +104,10 @@ public class NotificationController extends BaseController {
private final NotificationSettingsService notificationSettingsService;
@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" +
"The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" +
"Subscription command for unread notifications count:\n" +
@ -144,11 +158,17 @@ public class NotificationController extends BaseController {
"}\n```")
@GetMapping("/notifications")
@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,
@ApiParam(value = "Case-insensitive 'substring' filter based on notification subject or text")
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions
@ -156,6 +176,9 @@ public class NotificationController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markNotificationAsRead(@PathVariable UUID id,
@ -165,6 +188,9 @@ public class NotificationController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
@ -172,6 +198,9 @@ public class NotificationController extends BaseController {
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}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteNotification(@PathVariable UUID id,
@ -181,6 +210,35 @@ public class NotificationController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
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));
}
@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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request,
@ApiParam(value = "Amount of the recipients to show in preview")
@RequestParam(defaultValue = "20") int recipientsPreviewSize,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationTemplate template;
@ -281,6 +345,9 @@ public class NotificationController extends BaseController {
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}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException {
@ -288,12 +355,21 @@ public class NotificationController extends BaseController {
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")
@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,
@ApiParam(value = "Case-insensitive 'substring' filed based on the used template name")
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
@ -301,6 +377,11 @@ public class NotificationController extends BaseController {
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}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings,
@ -320,6 +416,9 @@ public class NotificationController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
@ -328,6 +427,9 @@ public class NotificationController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
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;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
@ -44,6 +46,15 @@ import org.thingsboard.server.service.security.permission.Operation;
import javax.validation.Valid;
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;
@RestController
@ -55,6 +66,53 @@ public class NotificationRuleController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule,
@ -74,6 +132,11 @@ public class NotificationRuleController extends BaseController {
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}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException {
@ -81,12 +144,21 @@ public class NotificationRuleController extends BaseController {
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")
@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,
@ApiParam(value = "Case-insensitive 'substring' filter based on rule's name")
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
@ -94,6 +166,10 @@ public class NotificationRuleController extends BaseController {
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}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
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;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
@ -58,6 +59,14 @@ import java.util.List;
import java.util.UUID;
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.service.security.permission.Resource.NOTIFICATION;
@ -71,7 +80,28 @@ public class NotificationTargetController extends BaseController {
private final NotificationTargetService notificationTargetService;
@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)
@PostMapping("/target")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -89,7 +119,7 @@ public class NotificationTargetController extends BaseController {
}
@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)
@GetMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -99,12 +129,14 @@ public class NotificationTargetController extends BaseController {
}
@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)
@PostMapping("/target/recipients")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
@ -119,9 +151,13 @@ public class NotificationTargetController extends BaseController {
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"})
@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) {
// generic permission
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)",
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)
@GetMapping("/targets")
@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,
@ApiParam(value = "Case-insensitive 'substring' filed based on the target's name")
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
@ -144,6 +186,10 @@ public class NotificationTargetController extends BaseController {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize,
@ -158,7 +204,7 @@ public class NotificationTargetController extends BaseController {
}
@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." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/target/{id}")

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

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.settings.NotificationSettings;
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.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.PageLink;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -51,6 +52,14 @@ import javax.validation.Valid;
import java.util.List;
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;
@ -65,19 +74,44 @@ public class NotificationTemplateController extends BaseController {
private final SlackService slackService;
@ApiOperation(value = "Save notification template (saveNotificationTemplate)",
notes = "Create or update notification template.\n\n" +
"Example:\n" +
"```\n{\n \"name\": \"Hello to all my users\",\n" +
" \"notificationType\": \"Message from administrator\",\n" +
notes = "Creates or updates notification template." + NEW_LINE +
"Here is an example of template to send notification via Web, SMS and Slack:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"name\": \"Greetings\",\n" +
" \"notificationType\": \"GENERAL\",\n" +
" \"configuration\": {\n" +
" \"defaultTextTemplate\": \"Hello everyone\", # required if any of the templates' bodies is not set\n" +
" \"templates\": {\n" +
" \"PUSH\": {\n \"method\": \"PUSH\",\n \"body\": null # defaultTextTemplate will be used if body is not set\n },\n" +
" \"SMS\": {\n \"method\": \"SMS\",\n \"body\": null\n },\n" +
" \"EMAIL\": {\n \"method\": \"EMAIL\",\n \"body\": \"Non-default value for email notification: <body>Hello everyone</body>\",\n \"subject\": \"Message from administrator\"\n },\n" +
" \"SLACK\": {\n \"method\": \"SLACK\",\n \"body\": null,\n \"conversationType\": \"PUBLIC_CHANNEL\",\n \"conversationId\": \"U02LD7BJOU2\" # received from listSlackConversations API method\n }\n" +
" \"deliveryMethodsTemplates\": {\n" +
" \"WEB\": {\n" +
" \"enabled\": true,\n" +
" \"subject\": \"Greetings\",\n" +
" \"body\": \"Hi there, ${recipientTitle}\",\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" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/template")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -88,7 +122,7 @@ public class NotificationTemplateController extends BaseController {
}
@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)
@GetMapping("/template/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -98,15 +132,22 @@ public class NotificationTemplateController extends BaseController {
}
@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)
@GetMapping("/templates")
@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,
@ApiParam(value = "Case-insensitive 'substring' filter based on template's name and notification type")
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = "Comma-separated list of notification types to filter the templates")
@RequestParam(required = false) NotificationType[] notificationTypes,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
@ -119,7 +160,7 @@ public class NotificationTemplateController extends BaseController {
}
@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." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/template/{id}")
@ -131,12 +172,12 @@ public class NotificationTemplateController extends BaseController {
}
@ApiOperation(value = "List Slack conversations (listSlackConversations)",
notes = "List available Slack conversations by type to use in notification template.\n\n" +
"Slack must be configured in notification settings." +
notes = "List available Slack conversations by type." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/slack/conversations")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
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,
@AuthenticationPrincipal SecurityUser user) {
// 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
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.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 javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ApiIgnore
@RestController
@TbCoreComponent
@RequestMapping("/api")
@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)
private BuildProperties buildProperties;
@Autowired
private EntitiesVersionControlService versionControlService;
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@ -56,6 +88,62 @@ public class SystemInfoController {
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() {
ObjectMapper objectMapper = new ObjectMapper();
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}")
private Boolean loadDemo;
@Value("${state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@Autowired
private EntityDatabaseSchemaService entityDatabaseSchemaService;
@ -247,6 +250,7 @@ public class ThingsboardInstallService {
case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) {
@ -272,6 +276,8 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Installing DataBase schema for timeseries...");
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;
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);
}
@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.NotificationRequestStatus;
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.settings.NotificationSettings;
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);
}
@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
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId 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)
|| entityType.equals(EntityType.TENANT_PROFILE)
|| entityType.equals(EntityType.DEVICE_PROFILE)
|| (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| entityType.equals(EntityType.ASSET_PROFILE)
|| entityType.equals(EntityType.API_USAGE_STATE)
|| (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.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -294,7 +295,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return;
}
if (inactivityTimeout <= 0L) {
inactivityTimeout = defaultInactivityTimeoutInSec;
inactivityTimeout = defaultInactivityTimeoutMs;
}
log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout);
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
@ -442,6 +443,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>();
partitionedEntities.forEach((tpi, deviceIds) -> {
log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
Set<DeviceId> idsFromRemovedTenant = new HashSet<>();
for (DeviceId deviceId : deviceIds) {
DeviceStateData stateData;
try {
@ -453,9 +455,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
try {
updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) {
idsFromRemovedTenant.add(deviceId);
continue;
} else {
log.warn("[{}] Failed to update inactivity state [{}]", deviceId, e.getMessage());
}
}
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
@ -466,6 +470,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
tenantDevicesActivity.getRight().incrementAndGet();
}
}
deviceIds.removeAll(idsFromRemovedTenant);
});
devicesActivity.forEach((tenantId, tenantDevicesActivity) -> {
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.stereotype.Service;
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.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId;
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.tenant.profile.DefaultTenantProfileConfiguration;
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}")
public void cleanUp() {
PageLink tenantsBatchRequest = new PageLink(10_000, 0);
PageLink removalBatchRequest = new PageLink(removalBatchSize, 0 );
PageData<TenantId> tenantsIds;
do {
tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest);
for (TenantId tenantId : tenantsIds.getData()) {
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;
}
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
for (TenantId tenantId : tenants) {
try {
cleanUp(tenantId);
} catch (Exception e) {
log.warn("Failed to clean up alarms by ttl for tenant {}", tenantId, e);
}
}
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
private void cleanUp(TenantId tenantId) {
if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
return;
}
long totalRemoved = 0;
while (true) {
PageData<AlarmId> toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest);
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);
});
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) {
return;
}
totalRemoved += toRemove.getTotalElements();
if (!toRemove.hasNext()) {
break;
}
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
if (totalRemoved > 0) {
log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
PageLink removalBatchRequest = new PageLink(removalBatchSize, 0);
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++;
}
}
tenantsBatchRequest = tenantsBatchRequest.nextPageLink();
} while (tenantsIds.hasNext());
if (!toRemove.hasNext()) {
break;
}
}
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
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}"
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}"
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.StringUtils;
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.asset.AssetProfile;
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.DeviceCredentials;
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.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService;
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.rest.LoginRequest;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
@ -123,6 +128,7 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@ -202,6 +208,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
private TenantProfileService tenantProfileService;
@Autowired
private TbTenantProfileService tbTenantProfileService;
@Autowired
public TimeseriesService tsService;
@ -903,18 +912,27 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
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.setAccessible(true);
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.setAccessible(true);
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) {
DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
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");
}
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.server.common.data.Customer;
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.EntitySubtype;
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());
testNotificationUpdateGatewayNever();
pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
PAGE_DATA_DEVICE_TYPE_REF, new PageLink(100));
PageData<DeviceInfo> pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(1, pageData.getData().size());
@ -1303,7 +1304,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
testNotificationUpdateGatewayNever();
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());
}

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.DataConstants;
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.OtaPackageInfo;
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());
Device device = doGet("/api/device/" + deviceUUID, Device.class);
Assert.assertNotNull(device);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {}, new PageLink(100)).getData();
Assert.assertTrue(edgeDevices.contains(device));
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {}, new PageLink(100)).getData();
Assert.assertTrue(edgeDevices.stream().map(DeviceInfo::getId).anyMatch(id -> id.equals(device.getId())));
testAutoGeneratedCodeByProtobuf(deviceUpdateMsg);
}
@ -487,10 +488,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
protected Device findDeviceByName(String deviceName) throws Exception {
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {
}, 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());
Device device = foundDevice.get();
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.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
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.OtaPackageInfo;
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.page.PageData;
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.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType;
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.DeviceCredentialsRequestMsg;
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\"}",
"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");
}
@Test
public void testSendDeleteDeviceOnEdgeToCloud() throws Exception {
Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge();
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
deviceDeleteMsgBuilder.setIdMSB(device.getId().getId().getMostSignificantBits());
deviceDeleteMsgBuilder.setIdLSB(device.getId().getId().getLeastSignificantBits());
deviceDeleteMsgBuilder.setIdMSB(savedDevice.getId().getId().getMostSignificantBits());
deviceDeleteMsgBuilder.setIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder);
upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build());
@ -398,12 +403,12 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
device = doGet("/api/device/" + device.getUuidId(), Device.class);
Assert.assertNotNull(device);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {
DeviceInfo deviceInfo = doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class);
Assert.assertNotNull(deviceInfo);
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeDevices.contains(device));
Assert.assertFalse(edgeDevices.contains(deviceInfo));
}
@Test
@ -455,7 +460,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value"));
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());
@ -599,7 +605,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
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);
});
@ -656,7 +663,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
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,
new TypeReference<>() {});
new TypeReference<>() {
});
}
@Test
@ -713,7 +721,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
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");
});

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

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.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.SparkplugMessageType.NCMD;
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 {
clientWithCorrectNodeAccessTokenWithNDEATH();
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<>();
await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS)
@ -424,7 +424,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception {
long ts = calendar.getTimeInMillis();
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<>();
await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS)

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

@ -148,6 +148,7 @@ message PostTelemetryMsg {
message PostAttributeMsg {
repeated KeyValueProto kv = 1;
bool shared = 2;
}
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.DeviceIdInfo;
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.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
@ -66,7 +67,7 @@ public interface DeviceService extends EntityDaoService {
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);
@ -76,10 +77,6 @@ public interface DeviceService extends EntityDaoService {
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);
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<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, 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);
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.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.DeviceId;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
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)
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)
private boolean customerIsPublic;
@ApiModelProperty(position = 15, value = "Name of the corresponding Device Profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String deviceProfileName;
@ApiModelProperty(position = 16, value = "Device active flag.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private boolean active;
public DeviceInfo() {
super();
@ -39,10 +45,11 @@ public class DeviceInfo extends Device {
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);
this.customerTitle = customerTitle;
this.customerIsPublic = customerIsPublic;
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 lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.EntityViewId;
@Data
@EqualsAndHashCode(callSuper = true)
public class EntityViewInfo extends EntityView {
@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.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.AssetId;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
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)
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)

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;
@Valid
private NotificationInfo info;
@NotNull
@Valid
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
public enum NotificationRuleTriggerType {
ENTITY_ACTION,
ALARM,
ALARM_COMMENT,
ALARM_ASSIGNMENT,
DEVICE_ACTIVITY,
ENTITY_ACTION,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
ALARM_ASSIGNMENT,
NEW_PLATFORM_VERSION(false),
ENTITIES_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!");
}
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray());
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable 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) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray());
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
} catch (Throwable 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 {
List<TransportProtos.PostTelemetryMsg> msgs = new ArrayList<>();
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) {
if (attributesMetricNames == null || !attributesMetricNames.contains(protoMetric.getName())) {
if (attributesMetricNames == null || !matches(attributesMetricNames, protoMetric)) {
long ts = protoMetric.getTimestamp();
String key = "bdSeq".equals(protoMetric.getName()) ?
topicTypeName + " " + protoMetric.getName() : protoMetric.getName();
@ -264,7 +264,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
try {
List<TransportApiProtos.AttributesMsg> msgs = new ArrayList<>();
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) {
if (attributesMetricNames.contains(protoMetric.getName())) {
if (matches(attributesMetricNames, protoMetric)) {
TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder();
Optional<TransportProtos.PostAttributeMsg> msgOpt = getPostAttributeMsg(protoMetric);
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 {
Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric);
if (keyValueProtoOpt.isPresent()) {
TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder();
builder.addKv(keyValueProtoOpt.get());
builder.setShared(true);
return Optional.of(builder.build());
}
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 {
TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes);
List<TransportProtos.KeyValueProto> kvList = proto.getKvList();
if (!CollectionUtils.isEmpty(kvList)) {
public static TransportProtos.PostAttributeMsg validatePostAttributeMsg(TransportProtos.PostAttributeMsg msg) throws IllegalArgumentException, InvalidProtocolBufferException {
if (!CollectionUtils.isEmpty(msg.getKvList())) {
byte[] bytes = msg.toByteArray();
TransportProtos.PostAttributeMsg proto = TransportProtos.PostAttributeMsg.parseFrom(bytes);
List<TransportProtos.KeyValueProto> kvList = proto.getKvList();
List<TransportProtos.KeyValueProto> keyValueProtos = validateKeyValueProtos(kvList);
TransportProtos.PostAttributeMsg.Builder result = TransportProtos.PostAttributeMsg.newBuilder();
result.addAllKv(keyValueProtos);
result.setShared(msg.getShared());
return result.build();
} else {
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();
metaData.putValue("deviceName", sessionInfo.getDeviceName());
metaData.putValue("deviceType", sessionInfo.getDeviceType());
if (msg.getShared()) {
metaData.putValue(DataConstants.SCOPE, DataConstants.SHARED_SCOPE);
}
metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "false");
CustomerId customerId = getCustomerId(sessionInfo);
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.PageRequest;
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.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
@ -128,4 +129,11 @@ public abstract class DaoUtil {
} 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 org.thingsboard.server.common.data.Device;
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.EntitySubtype;
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);
/**
* 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.
*
@ -100,26 +92,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
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.
*
@ -155,16 +127,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/
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.
*
@ -176,28 +138,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/
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.
*
@ -277,5 +217,5 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
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.DeviceIdInfo;
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.DeviceProfileType;
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.event.EventService;
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.PaginatedRemover;
@ -342,12 +344,17 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
log.trace("Executing findDeviceInfosByFilter, filter [{}], pageLink [{}]", filter, pageLink);
if (filter == null) {
throw new IncorrectParameterException("Filter is empty!");
}
validateId(filter.getTenantId(), INCORRECT_TENANT_ID + filter.getTenantId());
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink);
return deviceDao.findDeviceInfosByFilter(filter, pageLink);
}
@Override
@ -387,24 +394,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
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
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> 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);
}
@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
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink 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);
}
@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
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> 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_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_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";
@ -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_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.
*/

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

@ -28,4 +28,5 @@ public interface ToData<T> {
* @return the dto object
*/
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;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Immutable
@Table(name = ModelConstants.DEVICE_INFO_VIEW_COLUMN_FAMILY_NAME)
public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> {
public static final Map<String,String> deviceInfoColumnMap = new HashMap<>();
static {
deviceInfoColumnMap.put("customerTitle", "c.title");
deviceInfoColumnMap.put("deviceProfileName", "p.name");
}
@Column(name = ModelConstants.DEVICE_CUSTOMER_TITLE_PROPERTY)
private String customerTitle;
@Column(name = ModelConstants.DEVICE_CUSTOMER_IS_PUBLIC_PROPERTY)
private boolean customerIsPublic;
@Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_NAME_PROPERTY)
private String deviceProfileName;
@Column(name = ModelConstants.DEVICE_ACTIVE_PROPERTY)
private boolean active;
public DeviceInfoEntity() {
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
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.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.alarmComment, tenantAdmins.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")
.build())
.build();
public static final DefaultNotification deviceAction = DefaultNotification.builder()
.name("Device action notification")
public static final DefaultNotification entityAction = DefaultNotification.builder()
.name("Entity action notification")
.type(NotificationType.ENTITY_ACTION)
.subject("${entityType} was ${actionType}")
.text("${entityType} '${entityName}' was ${actionType} by user ${userEmail}")
.icon("info").color(null)
.button("Go to device").link("/devices/${entityId}")
.button("Go to ${entityType:lowerCase}").link("/${entityType:lowerCase}s/${entityId}")
.rule(DefaultRule.builder()
.name("Device created")
.triggerConfig(EntityActionNotificationRuleTriggerConfig.builder()
@ -341,7 +341,7 @@ public class DefaultNotifications {
public NotificationRule toRule(NotificationTemplateId templateId, NotificationTargetId... targets) {
DefaultRule defaultRule = this.rule;
NotificationRule rule = new NotificationRule();
rule.setName(name);
rule.setName(defaultRule.getName());
rule.setTemplateId(templateId);
rule.setTriggerType(defaultRule.getTriggerConfig().getTriggerType());
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());
EntityId affectedEntity = query.getAffectedEntityId();
AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) {
return DaoUtil.toPageData(
alarmRepository.findAlarms(
@ -153,7 +149,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -168,7 +164,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
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) {
log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink());
AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData(
alarmRepository.findCustomerAlarms(
tenantId.getId(),
@ -194,7 +186,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
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<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) {
return DaoUtil.toPageData(
alarmRepository.findAlarmsV2(
@ -226,7 +214,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -243,7 +231,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
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<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData(
alarmRepository.findCustomerAlarmsV2(
tenantId.getId(),
@ -273,7 +257,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
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.repository.query.Param;
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.model.sql.DeviceEntity;
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.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
*/
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) " +
"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")
@Query("SELECT d FROM DeviceInfoEntity d WHERE d.id = :deviceId")
DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId);
@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,
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 " +
@Query("SELECT d FROM DeviceInfoEntity d " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))")
@ -79,19 +68,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@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 (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 " +
"AND d.type = :type " +
"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 LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceEntity> findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.deviceProfileId = :deviceProfileId " +
@ -130,34 +106,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
Long countByTenantIdAndDeviceProfileIdAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId,
@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 " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
@ -168,33 +116,26 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@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 " +
@Query("SELECT d FROM DeviceInfoEntity d " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@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.customerId = :customerId " +
"AND d.deviceProfileId = :deviceProfileId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
"AND (:customerId IS NULL OR d.customerId = uuid(:customerId)) " +
"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 ((:deviceType) IS NULL OR d.type = :deviceType) " +
"AND (:deviceProfileId IS NULL OR d.deviceProfileId = uuid(:deviceProfileId)) " +
"AND ((:filterByActive) IS FALSE OR d.active = :deviceActive) " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.type) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.customerTitle) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByFilter(@Param("tenantId") UUID tenantId,
@Param("customerId") String customerId,
@Param("edgeId") String edgeId,
@Param("deviceType") String type,
@Param("deviceProfileId") String deviceProfileId,
@Param("filterByActive") boolean filterByActive,
@Param("deviceActive") boolean active,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId);
@ -234,13 +175,13 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
/**
* Count devices by tenantId.
* Custom query applied because default QueryDSL produces slow count(id).
* <p>
*
* There is two way to count devices.
* 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)
* - 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")
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;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
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.Pageable;
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.DeviceIdInfo;
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.EntitySubtype;
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.util.SqlDao;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -104,12 +110,18 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) {
public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantId(
tenantId,
deviceRepository.findDeviceInfosByFilter(
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(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
DaoUtil.toPageable(pageLink)));
}
@Override
@ -152,16 +164,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
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
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
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
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
@ -239,28 +221,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
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
public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID 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)
);
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;
CREATE VIEW alarm_info AS
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.Device;
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.EntitySubtype;
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();
device.setName(name);
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";
List<DeviceInfo> devicesTitle2 = new ArrayList<>();
@ -458,14 +459,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name);
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<>();
PageLink pageLink = new PageLink(15, 0, title1);
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle1.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -480,7 +481,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>();
pageLink = new PageLink(4, 0, title2);
do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle2.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -497,7 +498,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
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.assertEquals(0, pageData.getData().size());
@ -506,7 +507,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
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.assertEquals(0, pageData.getData().size());
}
@ -605,14 +606,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
device.setName("Device" + i);
device.setType("default");
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<>();
PageLink pageLink = new PageLink(23);
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId(customerId).build(), pageLink);
loadedDevices.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -627,7 +628,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
deviceService.unassignCustomerDevices(tenantId, customerId);
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.assertTrue(pageData.getData().isEmpty());
}
@ -848,7 +849,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithLabel).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue(
@ -862,7 +863,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithCustomer).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue(
@ -877,7 +878,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithType = new PageLink(100, 0, device.getType());
List<DeviceInfo> deviceInfosWithType = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithType).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithType).getData();
Assert.assertFalse(deviceInfosWithType.isEmpty());
Assert.assertTrue(
@ -907,7 +908,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
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.assertTrue(
@ -922,7 +923,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
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.assertTrue(
@ -953,7 +954,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
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.assertTrue(
@ -968,7 +969,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
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.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 sendBasicNotification(TenantId tenantId, UserId recipientId, String subject, String text);
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
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;
hasRepository: boolean;
tbelEnabled: boolean;
persistDeviceStateToTelemetry: boolean;
userSettings: UserSettings;
}
export interface SysParams extends SysParamsState {
maxDatapointsLimit: number;
}
export interface AuthPayload extends SysParamsState {
authUser: AuthUser;
userDetails: User;

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

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

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

@ -66,6 +66,11 @@ export const selectTbelEnabled = createSelector(
(state: AuthState) => state.tbelEnabled
);
export const selectPersistDeviceStateToTelemetry = createSelector(
selectAuthState,
(state: AuthState) => state.persistDeviceStateToTelemetry
);
export const selectUserSettings = createSelector(
selectAuthState,
(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 { 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 { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
@ -31,25 +31,17 @@ import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated
import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors';
import { Authority } from '@shared/models/authority.enum';
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 { AuthUser } from '@shared/models/user.model';
import { TimeService } from '@core/services/time.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 { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
import { isMobileApp } from '@core/utils';
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.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({
providedIn: 'root'
@ -65,10 +57,7 @@ export class AuthService {
private route: ActivatedRoute,
private zone: NgZone,
private utils: UtilsService,
private dashboardService: DashboardService,
private adminService: AdminService,
private translate: TranslateService,
private userSettingsService: UserSettingsService,
private dialog: MatDialog
) {
}
@ -400,7 +389,7 @@ export class AuthService {
authPayload.forceFullscreen = true;
}
if (authPayload.authUser.isPublic) {
this.loadSystemParams(authPayload).subscribe(
this.loadSystemParams().subscribe(
(sysParams) => {
authPayload = {...authPayload, ...sysParams};
loadUserSubject.next(authPayload);
@ -421,7 +410,7 @@ export class AuthService {
if (this.userForceFullscreen(authPayload)) {
authPayload.forceFullscreen = true;
}
this.loadSystemParams(authPayload).subscribe(
this.loadSystemParams().subscribe(
(sysParams) => {
authPayload = {...authPayload, ...sysParams};
let userLang;
@ -455,57 +444,14 @@ export class AuthService {
return loadUserSubject;
}
private loadIsUserTokenAccessEnabled(authUser: AuthUser): Observable<boolean> {
if (authUser.authority === Authority.SYS_ADMIN ||
authUser.authority === Authority.TENANT_ADMIN) {
return this.http.get<boolean>('/api/user/tokenAccessEnabled', defaultHttpOptions());
} else {
return of(false);
}
}
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({}))));
private loadSystemParams(): Observable<SysParamsState> {
return this.http.get<SysParams>('/api/system/params', defaultHttpOptions()).pipe(
map((sysParams) => {
this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit);
return sysParams;
}),
catchError(() => of({} as SysParamsState))
);
}
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,
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -42,6 +42,11 @@ export class DeviceService {
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 = '',
config?: RequestConfig): Observable<PageData<DeviceInfo>> {
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',
type: 'link',
path: '/alarms',
icon: 'notifications'
icon: 'mdi:alert-outline',
isMdiIcon: true
},
{
id: 'dashboards',
@ -813,7 +814,8 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'notifications'
icon: 'mdi:alert-outline',
isMdiIcon: true
},
{
id: 'dashboards',

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

@ -53,17 +53,11 @@ export class TimeService {
private http: HttpClient
) {}
public loadMaxDatapointsLimit(): Observable<number> {
return this.http.get<number>('/api/dashboard/maxDatapointsLimit',
defaultHttpOptions(true)).pipe(
map((limit) => {
this.maxDatapointsLimit = limit;
if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) {
this.maxDatapointsLimit = MIN_LIMIT + 1;
}
return this.maxDatapointsLimit;
})
);
public setMaxDatapointsLimit(limit: number) {
this.maxDatapointsLimit = limit;
if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) {
this.maxDatapointsLimit = MIN_LIMIT + 1;
}
}
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();
}
const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel',
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',

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

@ -160,17 +160,41 @@
</ng-container>
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'attribute.key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.key }}
<mat-cell *matCellDef="let attribute" class="attribute">
<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>
</ng-container>
<ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'attribute.value' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute"
class="tb-value-cell"
class="tb-value-cell attribute"
(click)="editAttribute($event, attribute)">
<div fxLayout="row" style="align-items: center">
<span style="overflow: hidden;text-overflow: ellipsis;" fxFlex>{{attribute.value | tbJson}}</span>
<div fxFlex fxLayout="row" style="align-items: center">
<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)">
<mat-icon>edit</mat-icon>
</span>

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

@ -51,6 +51,7 @@
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
}
}
@ -65,6 +66,22 @@
overflow: hidden;
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.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.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 { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
@NgModule({
declarations:
@ -282,6 +283,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
AssetProfileComponent,
AssetProfileDialogComponent,
AssetProfileAutocompleteComponent,
@ -425,6 +427,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
AssetProfileComponent,

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

@ -27,6 +27,10 @@
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
</mat-checkbox>
<button mat-icon-button color="primary"
[disabled]="isLoading$ | async"
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 {
margin-bottom: 8px;
color: rgba(0,0,0,0.54);
font-size: 12px;
}
@ -61,7 +60,6 @@
}
.add-entity-form .boolean-value-input .checkbox-label {
margin-bottom: 8px;
color: rgba(0,0,0,0.54);
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"-->
<!-- aria-label="Remove"-->
<!-- type="button"-->
<!-- (click)="removeOldRelation(i)"-->
<!-- (click)="removeOldRelation(i, relation.value)"-->
<!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">-->
<!-- <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.removeOldRelation = function(index) {
// vm.removeOldRelation = function(index, relation) {
// vm.oldRelations().removeAt(index);
// vm.relationsToDelete.push(vm.oldRelationsData[index]);
// vm.relationsToDelete.push(relation);
// 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()
export class CustomDialogService {
private customModules: Array<Type<any>>
constructor(
private translate: TranslateService,
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,
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(
class CustomDialogComponentInstance extends CustomDialogComponent {},
template,
[this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]).pipe(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((factory) => {
const dialogData: CustomDialogContainerData = {
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 config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.panelClass = 'tb-alarm-filter-panel';
config.panelClass = 'tb-filter-panel';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',

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

@ -67,4 +67,50 @@
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-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</div>
</div>
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<ng-template [ngSwitchCase]="'last'">
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;">
<table mat-table
[dataSource]="lastVisitedDashboardsDataSource" matSort
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property"
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="starred">
<mat-header-cell class="star-cell" *matHeaderCellDef>
</mat-header-cell>
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard">
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)"
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.last-viewed' | translate }}
</mat-header-cell>
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard">
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row>
</table>
<ng-container *ngIf="hasDashboardsAccess; else noDataAvailable">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</div>
<ng-template #noLastVisitedDashboards>
<div class="tb-no-dashboards">
<div class="tb-no-last-visited-bg"></div>
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
</div>
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<ng-template [ngSwitchCase]="'last'">
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;">
<table mat-table
[dataSource]="lastVisitedDashboardsDataSource" matSort
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property"
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="starred">
<mat-header-cell class="star-cell" *matHeaderCellDef>
</mat-header-cell>
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard">
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)"
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.last-viewed' | translate }}
</mat-header-cell>
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard">
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row>
</table>
</div>
</ng-template>
</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>
<ng-template #noLastVisitedDashboards>
<div class="tb-no-dashboards">
<div class="tb-no-last-visited-bg"></div>
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</ng-template>
</ng-template>
<ng-template [ngSwitchCase]="'starred'">
<div style="overflow-y: auto;">
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred">
<div class="tb-cell star-cell">
<mat-icon (click)="toggleDashboardStar(dashboard)"
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</div>
</div>
</div>
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete"
#starDashboardAutocomplete
subscriptSizing="dynamic"
appearance="outline"
[useIdValue]="false"
label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete>
</ng-template>
</ng-container>
<ng-template #loading>
<div class="tb-no-dashboards">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner>
</div>
<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>
<ng-template #noDataAvailable>
<div class="tb-card-header">
<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>
</ng-template>
</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;
starredDashboardValue = null;
hasDashboardsAccess = true;
dirty = false;
@ -84,7 +85,10 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
}
ngOnInit() {
this.reload();
this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) {
this.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-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
<ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-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 class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-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 class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar>
</div>
</div>
</div>
</div>
</ng-template>
</ng-template>
</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>

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} = {};
apiCallItemCritical: {[key: string]: boolean} = {};
hasUsageInfoAccess = true;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe,
@ -52,41 +54,43 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
}
ngOnInit() {
(this.authUser.authority === Authority.TENANT_ADMIN ?
this.usageInfoService.getUsageInfo() : of(null)).subscribe(
(usageInfo) => {
this.usageInfo = usageInfo;
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let entitiesHasCriticalItem = false;
let apiCallsHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) {
if (this.entityItemCritical[key]) {
entitiesHasCriticalItem = true;
break;
this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN;
if (this.hasUsageInfoAccess) {
this.usageInfoService.getUsageInfo().subscribe(
(usageInfo) => {
this.usageInfo = usageInfo;
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let entitiesHasCriticalItem = false;
let apiCallsHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) {
if (this.entityItemCritical[key]) {
entitiesHasCriticalItem = true;
break;
}
}
}
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
}
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
);
);
}
}
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 { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-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 {
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 { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.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 { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-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
],
providers: [
CustomDialogService,
ImportExportService,
{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,
OnDestroy,
OnInit,
Optional,
Renderer2,
SimpleChanges,
Type,
ViewChild,
ViewContainerRef,
ViewEncapsulation
@ -89,7 +91,7 @@ import {
import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute, Router } from '@angular/router';
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 { ActionNotificationShow } from '@core/notification/notification.actions';
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 { TbPopoverService } from '@shared/components/popover.service';
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({
selector: 'tb-widget',
@ -190,6 +194,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private popoverService: TbPopoverService,
@Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
@Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
private widgetService: WidgetService,
private resources: ResourcesService,
private timeService: TimeService,
@ -1160,8 +1165,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (isDefined(customHtml) && customHtml.length > 0) {
htmlTemplate = customHtml;
}
this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe(
() => {
this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({
next: () => {
if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) {
try {
if (!additionalParams) {
@ -1169,16 +1174,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId',
'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction);
this.widgetContext.customDialog.setAdditionalModules(descriptor.customModules);
customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
}
},
(errorMessages: string[]) => {
error: (errorMessages: string[]) => {
this.processResourcesLoadErrors(errorMessages);
}
);
});
break;
case WidgetActionType.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) {
this.cssParser.cssPreviewNamespace = actionNamespace;
this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace');
}
const resourceTasks: Observable<string>[] = [];
if (isDefined(customResources) && customResources.length > 0) {
customResources.forEach((resource) => {
resourceTasks.push(
this.resources.loadResource(resource.url).pipe(
catchError(e => of(`Failed to load custom action resource: '${resource.url}'`))
)
);
customResources.forEach(resource => {
if (resource.isModule) {
modulesTasks.push(
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 => {
let errors: string[];
if (msgs && msgs.length) {
errors = msgs.filter(msg => msg && msg.length > 0);
if (modulesTasks.length) {
const modulesObservable: Observable<string | Type<any>[]> = forkJoin(modulesTasks).pipe(
map(res => {
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 {
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 {
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',
breadcrumb: {
label: 'alarm.alarms',
icon: 'notifications'
icon: 'mdi:alert-outline'
},
isPage: true,
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 { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
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 { DashboardService } from '@core/http/dashboard.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 { 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()
export class DashboardResolver implements Resolve<Dashboard> {
constructor(private dashboardService: DashboardService,
constructor(private store: Store<AppState>,
private dashboardService: DashboardService,
private userSettingService: UserSettingsService,
private dashboardUtils: DashboardUtilsService) {
}
@ -42,7 +46,10 @@ export class DashboardResolver implements Resolve<Dashboard> {
const dashboardId = route.params.dashboardId;
return this.dashboardService.getDashboard(dashboardId).pipe(
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) => 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.
-->
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
[ngModel]="entitiesTableConfig.componentsData.deviceProfileId"
(ngModelChange)="deviceProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<tb-device-info-filter [ngModel]="entitiesTableConfig.componentsData.deviceInfoFilter"
(ngModelChange)="deviceInfoFilterChanged($event)">
</tb-device-info-filter>

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 { AppState } from '@core/core.state';
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 { DeviceProfileId } from '../../../../shared/models/id/device-profile-id';
@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<Devic
super(store);
}
deviceProfileChanged(deviceProfileId: DeviceProfileId) {
this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId;
deviceInfoFilterChanged(deviceInfoFilter: DeviceInfoFilter) {
this.entitiesTableConfig.componentsData.deviceInfoFilter = deviceInfoFilter;
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 { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import {
CellActionDescriptor,
checkBoxCell,
@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.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 { forkJoin, Observable, of, Subject } from 'rxjs';
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 { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
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 {
AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData
} 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()
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.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => {
return this.deviceService.saveDevice(device).pipe(
this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe(
tap(() => {
this.broadcast.broadcast('deviceSaved');
}),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
));
};
this.config.onEntityAction = action => this.onDeviceAction(action, this.config);
this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.onLoadAction = (route) => this.onLoadAction(route);
this.config.headerComponent = DeviceTableHeaderComponent;
@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
const routeParams = route.params;
this.config.componentsData = {
deviceScope: route.data.devicesType,
deviceProfileId: null,
deviceInfoFilter: {},
deviceCredentials$: new Subject<DeviceCredentials>(),
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.groupActionDescriptors = this.configureGroupActions(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.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
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>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name', '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') {
columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
entity => checkBoxCell(entity.customerIsPublic), () => ({})),
);
}
columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => {
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false)
);
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 {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink));
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);
} 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 {
this.config.entitiesFetchFunction = pageLink =>
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);
this.config.deleteEntity = () => of();
}
}
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>> {
@ -540,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
data: {
deviceId: device.id.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 => {
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',
data: {
breadcrumb: {
label: 'edge.edge-rulechains',
label: 'edge.rulechains',
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"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openInstructions')"
[fxShow]="!isEdit">
[fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon>info_outline</mat-icon>
<span>{{ 'edge.install-connect-instructions' | translate }}</span>
</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)
},
{
name: this.translate.instant('edge.edge-assets'),
name: this.translate.instant('edge.manage-assets'),
icon: 'domain',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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') {
actions.push(
{
name: this.translate.instant('edge.edge-assets'),
name: this.translate.instant('edge.manage-assets'),
icon: 'domain',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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',
isEnabled: (entity) => true,
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 { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs';
import { mergeMap, Observable, of } from 'rxjs';
import { HomeDashboard } from '@shared/models/dashboard.models';
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 { 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 tenantAdminHomePageDashboardJson from '!raw-loader!./tenant_admin_home_page.raw';
import customerUserHomePageDashboardJson from '!raw-loader!./customer_user_home_page.raw';
import { EntityKeyType } from '@shared/models/query/query.models';
@Injectable()
export class HomeDashboardResolver implements Resolve<HomeDashboard> {
@ -39,22 +44,43 @@ export class HomeDashboardResolver implements Resolve<HomeDashboard> {
resolve(): Observable<HomeDashboard> {
return this.dashboardService.getHomeDashboard().pipe(
map((dashboard) => {
mergeMap((dashboard) => {
if (!dashboard) {
let dashboard$: Observable<HomeDashboard>;
const authority = getCurrentAuthUser(this.store).authority;
switch (authority) {
case Authority.SYS_ADMIN:
dashboard = JSON.parse(sysAdminHomePageDashboardJson);
dashboard$ = of(JSON.parse(sysAdminHomePageDashboardJson));
break;
case Authority.TENANT_ADMIN:
dashboard = JSON.parse(tenantAdminHomePageDashboardJson);
dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(tenantAdminHomePageDashboardJson));
break;
case Authority.CUSTOMER_USER:
dashboard = JSON.parse(customerUserHomePageDashboardJson);
dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(customerUserHomePageDashboardJson));
break;
}
if (dashboard) {
dashboard.hideDashboardToolbar = true;
if (dashboard$) {
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;

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

@ -648,9 +648,9 @@
"padding": "16px",
"settings": {
"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,
"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",
"showTitleIcon": false,
@ -804,7 +804,7 @@
"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>",
"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",
"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') {
return this.translate.instant('edge.rulechain-templates');
} 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',
entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
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',
otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates',
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,
PowerMode
} 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 {
DEFAULT = 'DEFAULT',
@ -328,7 +331,7 @@ export interface DeviceProvisionConfiguration {
allowCreateNewDevicesByX509Certificate?: boolean;
}
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
export const createDeviceProfileConfiguration = (type: DeviceProfileType): DeviceProfileConfiguration => {
let configuration: DeviceProfileConfiguration = null;
if (type) {
switch (type) {
@ -339,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic
}
}
return configuration;
}
};
export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration {
export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => {
let configuration: DeviceConfiguration = null;
if (type) {
switch (type) {
@ -352,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig
}
}
return configuration;
}
};
export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration {
export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => {
let transportConfiguration: DeviceProfileTransportConfiguration = null;
if (type) {
switch (type) {
@ -368,6 +371,7 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
deviceAttributesTopic: 'v1/devices/me/attributes',
deviceAttributesSubscribeTopic: 'v1/devices/me/attributes',
sparkplug: false,
sparkplugAttributesMetricNames: ['Node Control/*', 'Device Control/*', 'Properties/*'],
sendAckOnValidationException: false,
transportPayloadTypeConfiguration: {
transportPayloadType: TransportPayloadType.JSON,
@ -408,9 +412,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
}
}
return transportConfiguration;
}
};
export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration {
export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => {
let transportConfiguration: DeviceTransportConfiguration = null;
if (type) {
switch (type) {
@ -446,7 +450,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
}
}
return transportConfiguration;
}
};
export enum AlarmConditionType {
SIMPLE = 'SIMPLE',
@ -489,7 +493,7 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
export interface AlarmSchedule{
dynamicValue?: {
sourceAttribute: string,
sourceAttribute: string;
sourceType: string;
};
type: AlarmScheduleType;
@ -516,17 +520,13 @@ interface AlarmRule {
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;
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 {
id: string;
@ -539,7 +539,7 @@ export interface DeviceProfileAlarm {
propagateRelationTypes?: Array<string>;
}
export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null {
export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => {
const deviceProfileAlarm: DeviceProfileAlarm = control.value;
if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType &&
deviceProfileAlarm.createRules) {
@ -564,7 +564,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio
}
}
return {deviceProfileAlarm: true};
}
};
export interface DeviceProfileData {
@ -718,6 +718,47 @@ export interface DeviceInfo extends Device {
customerTitle: string;
customerIsPublic: boolean;
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 {
@ -763,13 +804,11 @@ export interface DeviceCredentialMQTTBasic {
password: string;
}
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
return {
clientId: '',
userName: '',
password: ''
};
}
export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({
clientId: '',
userName: '',
password: ''
});
export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>;
@ -800,44 +839,23 @@ export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.sunday'
);
export function getDayString(day: number): string {
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 {
export const timeOfDayToUTCTimestamp = (date: Date | number): number => {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
};
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
}
export const utcTimestampToTimeOfDay = (time = 0): Date => 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) {
return _moment([1970, 0, 1, 0, 0, 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 end = timeOfDayToMoment(endsOn);
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>` +
` 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,
{
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';
import { PopoverPlacement } from '@shared/components/popover.models';
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 { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
@ -560,6 +560,7 @@ export interface CustomActionDescriptor {
customResources?: Array<WidgetResource>;
customHtml?: string;
customCss?: string;
customModules?: Type<any>[];
}
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>
<label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox>
</div>
</div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<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"
(ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2>
<h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox>
</div>
</div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary"
aria-label="Remove"
type="button"
(click)="removeOldRelation(i)"
(click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<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.removeOldRelation = function(index) {
vm.removeOldRelation = function(index, relation) {
vm.oldRelations().removeAt(index);
vm.relationsToDelete.push(vm.oldRelationsData[index]);
vm.relationsToDelete.push(relation);
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>
<label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox>
</div>
</div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<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"
(ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2>
<h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox>
</div>
</div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary"
aria-label="Remove"
type="button"
(click)="removeOldRelation(i)"
(click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')">

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

Loading…
Cancel
Save