Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into fix/alarm-rules

pull/14463/head
Viacheslav Klimov 8 months ago
parent
commit
83e40b8a5a
  1. 2
      application/src/main/data/json/system/widget_types/timeseries_table.json
  2. 69
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 14
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 17
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  5. 8
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java
  6. 54
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  7. 12
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  8. 69
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  9. 3
      application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java
  10. 44
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  11. 22
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  12. 28
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 9
      application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java
  14. 7
      application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java
  15. 46
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  16. 42
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  17. 23
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  18. 4
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  19. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  20. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  21. 19
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  22. 28
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  23. 100
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  24. 31
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  25. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java
  26. 5
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  27. 24
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  28. 155
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java
  29. 101
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  30. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java
  31. 95
      application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java
  32. 76
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  33. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  34. 12
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java
  35. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java
  36. 42
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  37. 2
      application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java
  38. 12
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  39. 2
      application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
  40. 14
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  41. 393
      application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java
  42. 47
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  43. 33
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java
  44. 4
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  45. 35
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  46. 2
      application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java
  47. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  48. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  49. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  50. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java
  51. 2
      common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java
  52. 20
      common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java
  53. 8
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  54. 2
      common/edge-api/src/main/proto/edge.proto
  55. 3
      dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java
  56. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java
  57. 3
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java
  58. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java
  59. 38
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  60. 1
      dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java
  61. 5
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  62. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  63. 37
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  64. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  65. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  66. 83
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  67. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  68. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  69. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  70. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  71. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  72. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  73. 31
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  74. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  75. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  76. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  77. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  78. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  79. 5
      ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html
  80. 4
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html
  81. 17
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss
  82. 5
      ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts
  83. 5
      ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html
  84. 4
      ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts
  85. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  86. 25
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  87. 34
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  88. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  89. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  90. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  91. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  92. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  93. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  94. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  95. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  96. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  97. 46
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  98. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  99. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  100. 155
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

2
application/src/main/data/json/system/widget_types/timeseries_table.json

@ -17,7 +17,7 @@
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config", "basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":[]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Time series table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"titleIcon\":null}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":[]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"sortOrder\":{\"property\":\"createdTime\",\"direction\":\"DESC\"}},\"title\":\"Time series table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"titleIcon\":null}"
}, },
"resources": [ "resources": [
{ {

69
application/src/main/data/upgrade/basic/schema_update.sql

@ -18,56 +18,27 @@
UPDATE tenant_profile UPDATE tenant_profile
SET profile_data = jsonb_set( SET profile_data = jsonb_set(
profile_data, profile_data,
'{configuration}', '{configuration}',
(profile_data -> 'configuration') jsonb_build_object(
|| jsonb_strip_nulls( 'minAllowedScheduledUpdateIntervalInSecForCF', 60,
jsonb_build_object( 'maxRelationLevelPerCfArgument', 10,
'minAllowedScheduledUpdateIntervalInSecForCF', 'maxRelatedEntitiesToReturnPerCfArgument', 100,
CASE 'minAllowedDeduplicationIntervalInSecForCF', 60,
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' 'minAllowedAggregationIntervalInSecForCF', 60
THEN NULL )
ELSE to_jsonb(60) ||
END, jsonb_strip_nulls(profile_data -> 'configuration')
'maxRelationLevelPerCfArgument', )
CASE
WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument'
THEN NULL
ELSE to_jsonb(10)
END,
'maxRelatedEntitiesToReturnPerCfArgument',
CASE
WHEN (profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument'
THEN NULL
ELSE to_jsonb(100)
END,
'minAllowedDeduplicationIntervalInSecForCF',
CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF'
THEN NULL
ELSE to_jsonb(60)
END,
'minAllowedAggregationIntervalInSecForCF',
CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedAggregationIntervalInSecForCF'
THEN NULL
ELSE to_jsonb(60)
END
)
),
false
)
WHERE NOT ( WHERE NOT (
(profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' jsonb_strip_nulls(profile_data -> 'configuration') ?& ARRAY[
AND 'minAllowedScheduledUpdateIntervalInSecForCF',
(profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' 'maxRelationLevelPerCfArgument',
AND 'maxRelatedEntitiesToReturnPerCfArgument',
(profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument' 'minAllowedDeduplicationIntervalInSecForCF',
AND 'minAllowedAggregationIntervalInSecForCF'
(profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF' ]
AND );
(profile_data -> 'configuration') ? 'minAllowedAggregationIntervalInSecForCF'
);
-- UPDATE TENANT PROFILE CONFIGURATION END -- UPDATE TENANT PROFILE CONFIGURATION END

14
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.actors; package org.thingsboard.server.actors;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -54,7 +55,6 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -119,7 +119,6 @@ import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService;
import org.thingsboard.server.service.cf.CalculatedFieldStateService; import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.OwnerService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
@ -144,14 +143,12 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@ -842,7 +839,7 @@ public class ActorSystemContext {
Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} }
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, Map<String, ArgumentEntry> arguments, UUID tbMsgId, TbMsgType tbMsgType, String result, String errorMessage) { public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, JsonNode arguments, UUID tbMsgId, String tbMsgType, String result, String errorMessage) {
if (checkLimits(tenantId)) { if (checkLimits(tenantId)) {
try { try {
CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder() CalculatedFieldDebugEvent.CalculatedFieldDebugEventBuilder eventBuilder = CalculatedFieldDebugEvent.builder()
@ -855,13 +852,10 @@ public class ActorSystemContext {
eventBuilder.msgId(tbMsgId); eventBuilder.msgId(tbMsgId);
} }
if (tbMsgType != null) { if (tbMsgType != null) {
eventBuilder.msgType(tbMsgType.name()); eventBuilder.msgType(tbMsgType);
} }
if (arguments != null) { if (arguments != null) {
eventBuilder.arguments(JacksonUtil.toString( eventBuilder.arguments(JacksonUtil.toString(arguments));
arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue()))
));
} }
if (result != null) { if (result != null) {
eventBuilder.result(result); eventBuilder.result(result);

17
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -70,6 +70,7 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
/** /**
@ -311,6 +312,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} catch (Exception e) { } catch (Exception e) {
log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e);
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
} }
@ -351,7 +355,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
if (state.isSizeOk()) { if (state.isSizeOk()) {
log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); log.debug("[{}][{}] Reevaluating CF state", entityId, cfId);
processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, null, msg.getCallback()); processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, REEVALUATION_MSG, msg.getCallback());
} else { } else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
} }
@ -432,7 +436,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (!updatedArgs.isEmpty() || justRestored) { if (!updatedArgs.isEmpty() || justRestored) {
cfIdList = new ArrayList<>(cfIdList); cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId()); cfIdList.add(ctx.getCfId());
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); String msgType = tbMsgType == null ? null : tbMsgType.name();
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, msgType, callback);
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
@ -474,7 +479,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx, private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx,
List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { List<CalculatedFieldId> cfIdList, UUID tbMsgId, String tbMsgType, TbCallback callback) throws CalculatedFieldException {
callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback);
log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs);
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
@ -492,19 +497,19 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onSuccess(); callback.onSuccess();
} }
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.stringValue(), null); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, calculationResult.stringValue(), null);
} }
} }
} else { } else {
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!";
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), tbMsgId, tbMsgType, null, errorMsg);
} }
callback.onSuccess(); callback.onSuccess();
} }
} catch (Exception e) { } catch (Exception e) {
log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e); log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e);
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArgumentsJson()).cause(e).build();
} finally { } finally {
if (!stateSizeChecked) { if (!stateSizeChecked) {
state.checkStateSize(ctxId, ctx.getMaxStateSize()); state.checkStateSize(ctxId, ctx.getMaxStateSize());

8
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldException.java

@ -15,14 +15,12 @@
*/ */
package org.thingsboard.server.actors.calculatedField; package org.thingsboard.server.actors.calculatedField;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@ -32,8 +30,8 @@ public class CalculatedFieldException extends Exception {
private final CalculatedFieldCtx ctx; private final CalculatedFieldCtx ctx;
private final EntityId eventEntity; private final EntityId eventEntity;
private final UUID msgId; private final UUID msgId;
private final TbMsgType msgType; private final String msgType;
private Map<String, ArgumentEntry> arguments; private JsonNode arguments;
private String errorMessage; private String errorMessage;
private Exception cause; private Exception cause;

54
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -35,9 +35,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -120,7 +118,7 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/serverTime") @GetMapping(value = "/dashboard/serverTime")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137")))
public long getServerTime() throws ThingsboardException { public long getServerTime() {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }
@ -132,7 +130,7 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/maxDatapointsLimit") @GetMapping(value = "/dashboard/maxDatapointsLimit")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000"))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000")))
public long getMaxDatapointsLimit() throws ThingsboardException { public long getMaxDatapointsLimit() {
return maxDatapointsLimit; return maxDatapointsLimit;
} }
@ -154,11 +152,11 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/{dashboardId}") @GetMapping(value = "/dashboard/{dashboardId}")
public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId, @PathVariable(DASHBOARD_ID) String strDashboardId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION) @Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources, @RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
checkParameter(DASHBOARD_ID, strDashboardId); checkParameter(DASHBOARD_ID, strDashboardId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
@ -179,9 +177,9 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/dashboard") @PostMapping(value = "/dashboard")
public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
@RequestBody Dashboard dashboard, @RequestBody Dashboard dashboard,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, @RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
dashboard.setTenantId(getTenantId()); dashboard.setTenantId(getTenantId());
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
var savedDashboard = tbDashboardService.save(dashboard, getCurrentUser()); var savedDashboard = tbDashboardService.save(dashboard, getCurrentUser());
@ -301,8 +299,7 @@ public class DashboardController extends BaseController {
"[assign Device to Public Customer](#!/device-controller/assignDeviceToPublicCustomerUsingPOST) for this purpose. " + "[assign Device to Public Customer](#!/device-controller/assignDeviceToPublicCustomerUsingPOST) for this purpose. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) @PostMapping(value = "/customer/public/dashboard/{dashboardId}")
@ResponseBody
public Dashboard assignDashboardToPublicCustomer( public Dashboard assignDashboardToPublicCustomer(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
@ -316,8 +313,7 @@ public class DashboardController extends BaseController {
notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " + notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH) "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/customer/public/dashboard/{dashboardId}")
@ResponseBody
public Dashboard unassignDashboardFromPublicCustomer( public Dashboard unassignDashboardFromPublicCustomer(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
@ -331,8 +327,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS +
SYSTEM_AUTHORITY_PARAGRAPH) SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"})
@ResponseBody
public PageData<DashboardInfo> getTenantDashboards( public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId, @PathVariable(TENANT_ID) String strTenantId,
@ -356,8 +351,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " notes = "Returns a page of dashboard info objects owned by the tenant of a current user. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/dashboards", params = {"pageSize", "page"})
@ResponseBody
public PageData<DashboardInfo> getTenantDashboards( public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -384,8 +378,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the specified customer. " notes = "Returns a page of dashboard info objects owned by the specified customer. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"})
@ResponseBody
public PageData<DashboardInfo> getCustomerDashboards( public PageData<DashboardInfo> getCustomerDashboards(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -454,8 +447,7 @@ public class DashboardController extends BaseController {
"If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) @GetMapping(value = "/dashboard/home/info")
@ResponseBody
public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException { public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser(); SecurityUser securityUser = getCurrentUser();
if (securityUser.isSystemAdmin()) { if (securityUser.isSystemAdmin()) {
@ -470,8 +462,7 @@ public class DashboardController extends BaseController {
notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the corresponding tenant. " + notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the corresponding tenant. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) @GetMapping(value = "/tenant/dashboard/home/info")
@ResponseBody
public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException { public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
Tenant tenant = tenantService.findTenantById(getTenantId()); Tenant tenant = tenantService.findTenantById(getTenantId());
JsonNode additionalInfo = tenant.getAdditionalInfo(); JsonNode additionalInfo = tenant.getAdditionalInfo();
@ -491,7 +482,7 @@ public class DashboardController extends BaseController {
notes = "Update the home dashboard assignment for the current tenant. " + notes = "Update the home dashboard assignment for the current tenant. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST) @PostMapping(value = "/tenant/dashboard/home/info")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void setTenantHomeDashboardInfo( public void setTenantHomeDashboardInfo(
@Parameter(description = "A JSON object that represents home dashboard id and other parameters", required = true) @Parameter(description = "A JSON object that represents home dashboard id and other parameters", required = true)
@ -540,8 +531,7 @@ public class DashboardController extends BaseController {
"Third, once dashboard will be delivered to edge service, it's going to be available for usage on remote edge instance." + "Third, once dashboard will be delivered to edge service, it's going to be available for usage on remote edge instance." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST) @PostMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}")
@ResponseBody
public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId, public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
@ -563,8 +553,7 @@ public class DashboardController extends BaseController {
"Third, once 'unassign' command will be delivered to edge service, it's going to remove dashboard locally." + "Third, once 'unassign' command will be delivered to edge service, it's going to remove dashboard locally." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}")
@ResponseBody
public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId, public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId); checkParameter(EDGE_ID, strEdgeId);
@ -583,8 +572,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects assigned to the specified edge. " notes = "Returns a page of dashboard info objects assigned to the specified edge. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"})
@ResponseBody
public PageData<DashboardInfo> getEdgeDashboards( public PageData<DashboardInfo> getEdgeDashboards(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,

12
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -53,7 +53,6 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -71,7 +70,6 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -268,7 +266,7 @@ public class EdgeController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink));
} else { } else {
return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink));
@ -295,7 +293,7 @@ public class EdgeController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink));
} else { } else {
return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink));
@ -359,7 +357,7 @@ public class EdgeController extends BaseController {
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageData<Edge> result; PageData<Edge> result;
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink);
} else { } else {
result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
@ -394,7 +392,7 @@ public class EdgeController extends BaseController {
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageData<EdgeInfo> result; PageData<EdgeInfo> result;
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink);
} else { } else {
result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
@ -470,7 +468,7 @@ public class EdgeController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/edge/sync/{edgeId}") @PostMapping(value = "/edge/sync/{edgeId}")
public DeferredResult<ResponseEntity> syncEdge(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) public DeferredResult<ResponseEntity> syncEdge(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { @PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
final DeferredResult<ResponseEntity> response = new DeferredResult<>(); final DeferredResult<ResponseEntity> response = new DeferredResult<>();
if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) { if (isEdgesEnabled() && edgeRpcServiceOpt.isPresent()) {

69
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -22,12 +22,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -84,9 +85,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
@ -102,8 +100,7 @@ public class EntityViewController extends BaseController {
notes = "Fetch the EntityView object based on the provided entity view id. " notes = "Fetch the EntityView object based on the provided entity view id. "
+ ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET) @GetMapping(value = "/entityView/{entityViewId}")
@ResponseBody
public EntityView getEntityViewById( public EntityView getEntityViewById(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
@ -115,8 +112,7 @@ public class EntityViewController extends BaseController {
notes = "Fetch the Entity View info object based on the provided Entity View Id. " notes = "Fetch the Entity View info object based on the provided Entity View Id. "
+ ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET) @GetMapping(value = "/entityView/info/{entityViewId}")
@ResponseBody
public EntityViewInfo getEntityViewInfoById( public EntityViewInfo getEntityViewInfoById(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
@ -130,8 +126,7 @@ public class EntityViewController extends BaseController {
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Entity View entity." + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Entity View entity." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView", method = RequestMethod.POST) @PostMapping(value = "/entityView")
@ResponseBody
public EntityView saveEntityView( public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.") @Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView, @RequestBody EntityView entityView,
@ -156,7 +151,7 @@ public class EntityViewController extends BaseController {
notes = "Delete the EntityView object based on the provided entity view id. " notes = "Delete the EntityView object based on the provided entity view id. "
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/entityView/{entityViewId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteEntityView( public void deleteEntityView(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@ -170,8 +165,7 @@ public class EntityViewController extends BaseController {
@ApiOperation(value = "Get Entity View by name (getTenantEntityView)", @ApiOperation(value = "Get Entity View by name (getTenantEntityView)",
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/entityViews", params = {"entityViewName"})
@ResponseBody
public EntityView getTenantEntityView( public EntityView getTenantEntityView(
@Parameter(description = "Entity View name") @Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException { @RequestParam String entityViewName) throws ThingsboardException {
@ -182,8 +176,7 @@ public class EntityViewController extends BaseController {
@ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)",
notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST) @PostMapping(value = "/customer/{customerId}/entityView/{entityViewId}")
@ResponseBody
public EntityView assignEntityViewToCustomer( public EntityView assignEntityViewToCustomer(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -204,8 +197,7 @@ public class EntityViewController extends BaseController {
@ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)", @ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)",
notes = "Clears assignment of the Entity View to customer. Customer will not be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) notes = "Clears assignment of the Entity View to customer. Customer will not be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/customer/entityView/{entityViewId}")
@ResponseBody
public EntityView unassignEntityViewFromCustomer( public EntityView unassignEntityViewFromCustomer(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
@ -225,8 +217,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View objects assigned to customer. " + notes = "Returns a page of Entity View objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityView> getCustomerEntityViews( public PageData<EntityView> getCustomerEntityViews(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -247,7 +238,7 @@ public class EntityViewController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type)); return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerIdAndType(tenantId, customerId, pageLink, type));
} else { } else {
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
@ -258,8 +249,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityViewInfo> getCustomerEntityViewInfos( public PageData<EntityViewInfo> getCustomerEntityViewInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -280,7 +270,7 @@ public class EntityViewController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ); checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else { } else {
return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
@ -291,8 +281,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/entityViews", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityView> getTenantEntityViews( public PageData<EntityView> getTenantEntityViews(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -309,7 +298,7 @@ public class EntityViewController extends BaseController {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type)); return checkNotNull(entityViewService.findEntityViewByTenantIdAndType(tenantId, pageLink, type));
} else { } else {
return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink)); return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink));
@ -320,8 +309,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityViewInfo> getTenantEntityViewInfos( public PageData<EntityViewInfo> getTenantEntityViewInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -337,7 +325,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink)); return checkNotNull(entityViewService.findEntityViewInfosByTenantIdAndType(tenantId, type, pageLink));
} else { } else {
return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink)); return checkNotNull(entityViewService.findEntityViewInfosByTenantId(tenantId, pageLink));
@ -349,8 +337,7 @@ public class EntityViewController extends BaseController {
"The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " + "The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityViews", method = RequestMethod.POST) @PostMapping(value = "/entityViews")
@ResponseBody
public List<EntityView> findByQuery( public List<EntityView> findByQuery(
@Parameter(description = "The entity view search query JSON") @Parameter(description = "The entity view search query JSON")
@RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { @RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
@ -374,8 +361,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a set of unique entity view types based on entity views that are either owned by the tenant or assigned to the customer which user is performing the request." notes = "Returns a set of unique entity view types based on entity views that are either owned by the tenant or assigned to the customer which user is performing the request."
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/types", method = RequestMethod.GET) @GetMapping(value = "/entityView/types")
@ResponseBody
public List<EntitySubtype> getEntityViewTypes() throws ThingsboardException, ExecutionException, InterruptedException { public List<EntitySubtype> getEntityViewTypes() throws ThingsboardException, ExecutionException, InterruptedException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
@ -388,8 +374,7 @@ public class EntityViewController extends BaseController {
"This is useful to create dashboards that you plan to share/embed on a publicly available website. " + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
"However, users that are logged-in and belong to different tenant will not be able to access the entity view." + TENANT_AUTHORITY_PARAGRAPH) "However, users that are logged-in and belong to different tenant will not be able to access the entity view." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/entityView/{entityViewId}", method = RequestMethod.POST) @PostMapping(value = "/customer/public/entityView/{entityViewId}")
@ResponseBody
public EntityView assignEntityViewToPublicCustomer( public EntityView assignEntityViewToPublicCustomer(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION) @Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
@ -406,8 +391,7 @@ public class EntityViewController extends BaseController {
EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once entity view will be delivered to edge service, it's going to be available for usage on remote edge instance.") "Third, once entity view will be delivered to edge service, it's going to be available for usage on remote edge instance.")
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST) @PostMapping(value = "/edge/{edgeId}/entityView/{entityViewId}")
@ResponseBody
public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId, public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId); checkParameter(EDGE_ID, strEdgeId);
@ -430,8 +414,7 @@ public class EntityViewController extends BaseController {
EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once 'unassign' command will be delivered to edge service, it's going to remove entity view locally.") "Third, once 'unassign' command will be delivered to edge service, it's going to remove entity view locally.")
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/edge/{edgeId}/entityView/{entityViewId}")
@ResponseBody
public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId, public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId); checkParameter(EDGE_ID, strEdgeId);
@ -448,8 +431,7 @@ public class EntityViewController extends BaseController {
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityView> getEdgeEntityViews( public PageData<EntityView> getEdgeEntityViews(
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@RequestParam int pageSize, @RequestParam int pageSize,
@ -466,7 +448,7 @@ public class EntityViewController extends BaseController {
checkEdgeId(edgeId, Operation.READ); checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
PageData<EntityView> nonFilteredResult; PageData<EntityView> nonFilteredResult;
if (type != null && type.trim().length() > 0) { if (type != null && !type.trim().isEmpty()) {
nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink);
} else { } else {
nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
@ -485,4 +467,5 @@ public class EntityViewController extends BaseController {
nonFilteredResult.hasNext()); nonFilteredResult.hasNext());
return checkNotNull(filteredResult); return checkNotNull(filteredResult);
} }
} }

3
application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java

@ -15,9 +15,6 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
/**
* Created by ashvayka on 17.05.18.
*/
public class TbUrlConstants { public class TbUrlConstants {
public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry"; public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry";
public static final String RPC_V1_URL_PREFIX = "/api/plugins/rpc"; public static final String RPC_V1_URL_PREFIX = "/api/plugins/rpc";

44
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -37,13 +37,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -131,10 +131,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_SC
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TS_STRICT_DATA_EXAMPLE; import static org.thingsboard.server.controller.ControllerConstants.TS_STRICT_DATA_EXAMPLE;
/**
* Created by ashvayka on 22.03.18.
*/
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX) @RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX)
@ -172,8 +168,7 @@ public class TelemetryController extends BaseController {
"\n * SHARED_SCOPE - supported for devices. " "\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/keys/attributes")
@ResponseBody
public DeferredResult<ResponseEntity> getAttributeKeys( public DeferredResult<ResponseEntity> getAttributeKeys(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
@ -187,8 +182,7 @@ public class TelemetryController extends BaseController {
"\n * SHARED_SCOPE - supported for devices. " "\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}")
@ResponseBody
public DeferredResult<ResponseEntity> getAttributeKeysByScope( public DeferredResult<ResponseEntity> getAttributeKeysByScope(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -205,8 +199,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/values/attributes")
@ResponseBody
public DeferredResult<ResponseEntity> getAttributes( public DeferredResult<ResponseEntity> getAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -229,8 +222,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}")
@ResponseBody
public DeferredResult<ResponseEntity> getAttributesByScope( public DeferredResult<ResponseEntity> getAttributesByScope(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -247,8 +239,7 @@ public class TelemetryController extends BaseController {
notes = "Returns a set of unique time series key names for the selected entity. " + notes = "Returns a set of unique time series key names for the selected entity. " +
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/keys/timeseries")
@ResponseBody
public DeferredResult<ResponseEntity> getTimeseriesKeys( public DeferredResult<ResponseEntity> getTimeseriesKeys(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
@ -269,8 +260,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/values/timeseries")
@ResponseBody
public DeferredResult<ResponseEntity> getLatestTimeseries( public DeferredResult<ResponseEntity> getLatestTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -294,8 +284,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"})
@ResponseBody
public DeferredResult<ResponseEntity> getTimeseries( public DeferredResult<ResponseEntity> getTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -418,8 +407,7 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}")
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityTelemetry( public DeferredResult<ResponseEntity> saveEntityTelemetry(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -442,8 +430,7 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}")
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL( public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -471,8 +458,7 @@ public class TelemetryController extends BaseController {
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @DeleteMapping(value = "/{entityType}/{entityId}/timeseries/delete")
@ResponseBody
public DeferredResult<ResponseEntity> deleteEntityTimeseries( public DeferredResult<ResponseEntity> deleteEntityTimeseries(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -553,8 +539,7 @@ public class TelemetryController extends BaseController {
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @DeleteMapping(value = "/{deviceId}/{scope}")
@ResponseBody
public DeferredResult<ResponseEntity> deleteDeviceAttributes( public DeferredResult<ResponseEntity> deleteDeviceAttributes(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@ -577,8 +562,7 @@ public class TelemetryController extends BaseController {
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @DeleteMapping(value = "/{entityType}/{entityId}/{scope}")
@ResponseBody
public DeferredResult<ResponseEntity> deleteEntityAttributes( public DeferredResult<ResponseEntity> deleteEntityAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,

22
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -21,12 +21,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
@ -70,8 +71,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenant (getTenantById)", @ApiOperation(value = "Get Tenant (getTenantById)",
notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET) @GetMapping(value = "/tenant/{tenantId}")
@ResponseBody
public Tenant getTenantById( public Tenant getTenantById(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException {
@ -86,8 +86,7 @@ public class TenantController extends BaseController {
notes = "Fetch the Tenant Info object based on the provided Tenant Id. " + notes = "Fetch the Tenant Info object based on the provided Tenant Id. " +
TENANT_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) TENANT_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET) @GetMapping(value = "/tenant/info/{tenantId}")
@ResponseBody
public TenantInfo getTenantInfoById( public TenantInfo getTenantInfoById(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException {
@ -105,8 +104,7 @@ public class TenantController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Tenant entity." + "Remove 'id', 'tenantId' from the request body example (below) to create new Tenant entity." +
SYSTEM_AUTHORITY_PARAGRAPH) SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant", method = RequestMethod.POST) @PostMapping(value = "/tenant")
@ResponseBody
public Tenant saveTenant(@Parameter(description = "A JSON value representing the tenant.") public Tenant saveTenant(@Parameter(description = "A JSON value representing the tenant.")
@RequestBody Tenant tenant) throws Exception { @RequestBody Tenant tenant) throws Exception {
checkEntity(tenant.getId(), tenant, Resource.TENANT); checkEntity(tenant.getId(), tenant, Resource.TENANT);
@ -116,7 +114,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Delete Tenant (deleteTenant)", @ApiOperation(value = "Delete Tenant (deleteTenant)",
notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/tenant/{tenantId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws Exception { @PathVariable(TENANT_ID) String strTenantId) throws Exception {
@ -128,8 +126,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenants", params = {"pageSize", "page"})
@ResponseBody
public PageData<Tenant> getTenants( public PageData<Tenant> getTenants(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -148,8 +145,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. "
+ TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenantInfos", params = {"pageSize", "page"})
@ResponseBody
public PageData<TenantInfo> getTenantInfos( public PageData<TenantInfo> getTenantInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,

28
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -23,13 +23,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityInfo;
@ -74,8 +74,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profile (getTenantProfileById)", @ApiOperation(value = "Get Tenant Profile (getTenantProfileById)",
notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET) @GetMapping(value = "/tenantProfile/{tenantProfileId}")
@ResponseBody
public TenantProfile getTenantProfileById( public TenantProfile getTenantProfileById(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -87,8 +86,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)", @ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)",
notes = "Fetch the Tenant Profile Info object based on the provided Tenant Profile Id. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Fetch the Tenant Profile Info object based on the provided Tenant Profile Id. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET) @GetMapping(value = "/tenantProfileInfo/{tenantProfileId}")
@ResponseBody
public EntityInfo getTenantProfileInfoById( public EntityInfo getTenantProfileInfoById(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -100,8 +98,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)", @ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)",
notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) @GetMapping(value = "/tenantProfileInfo/default")
@ResponseBody
public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException { public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException {
return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId())); return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId()));
} }
@ -180,8 +177,7 @@ public class TenantProfileController extends BaseController {
"Remove 'id', from the request body example (below) to create new Tenant Profile entity." + "Remove 'id', from the request body example (below) to create new Tenant Profile entity." +
SYSTEM_AUTHORITY_PARAGRAPH) SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @PostMapping(value = "/tenantProfile")
@ResponseBody
public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.")
@Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException { @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
TenantProfile oldProfile; TenantProfile oldProfile;
@ -198,7 +194,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)", @ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)",
notes = "Deletes the tenant profile. Referencing non-existing tenant profile Id will cause an error. Referencing profile that is used by the tenants will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Deletes the tenant profile. Referencing non-existing tenant profile Id will cause an error. Referencing profile that is used by the tenants will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/tenantProfile/{tenantProfileId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteTenantProfile(@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) public void deleteTenantProfile(@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -211,8 +207,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)", @ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)",
notes = "Makes specified tenant profile to be default. Referencing non-existing tenant profile Id will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Makes specified tenant profile to be default. Referencing non-existing tenant profile Id will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST) @PostMapping(value = "/tenantProfile/{tenantProfileId}/default")
@ResponseBody
public TenantProfile setDefaultTenantProfile( public TenantProfile setDefaultTenantProfile(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION) @Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -225,8 +220,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenantProfiles", params = {"pageSize", "page"})
@ResponseBody
public PageData<TenantProfile> getTenantProfiles( public PageData<TenantProfile> getTenantProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -245,8 +239,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. "
+ TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"})
@ResponseBody
public PageData<EntityInfo> getTenantProfileInfos( public PageData<EntityInfo> getTenantProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -269,5 +262,4 @@ public class TenantProfileController extends BaseController {
return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
} }
} }

9
application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java

@ -17,11 +17,9 @@ package org.thingsboard.server.controller;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -37,9 +35,8 @@ public class UiSettingsController extends BaseController {
notes = "Get UI help base url used to fetch help assets. " + notes = "Get UI help base url used to fetch help assets. " +
"The actual value of the base url is configurable in the system configuration file.") "The actual value of the base url is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/uiSettings/helpBaseUrl", method = RequestMethod.GET) @GetMapping(value = "/uiSettings/helpBaseUrl")
@ResponseBody public String getHelpBaseUrl() {
public String getHelpBaseUrl() throws ThingsboardException {
return helpBaseUrl; return helpBaseUrl;
} }

7
application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java

@ -18,9 +18,8 @@ package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -37,9 +36,9 @@ public class UsageInfoController extends BaseController {
private UsageInfoService usageInfoService; private UsageInfoService usageInfoService;
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/usage", method = RequestMethod.GET) @GetMapping(value = "/usage")
@ResponseBody
public UsageInfo getTenantUsageInfo() throws ThingsboardException { public UsageInfo getTenantUsageInfo() throws ThingsboardException {
return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId())); return checkNotNull(usageInfoService.getUsageInfo(getCurrentUser().getTenantId()));
} }
} }

46
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -33,9 +33,7 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -133,8 +131,7 @@ public class UserController extends BaseController {
"If the user has the authority of 'TENANT_ADMIN', the server checks that the requested user is owned by the same tenant. " + "If the user has the authority of 'TENANT_ADMIN', the server checks that the requested user is owned by the same tenant. " +
"If the user has the authority of 'CUSTOMER_USER', the server checks that the requested user is owned by the same customer.") "If the user has the authority of 'CUSTOMER_USER', the server checks that the requested user is owned by the same customer.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) @GetMapping(value = "/user/{userId}")
@ResponseBody
public User getUserById( public User getUserById(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId) throws ThingsboardException { @PathVariable(USER_ID) String strUserId) throws ThingsboardException {
@ -150,8 +147,7 @@ public class UserController extends BaseController {
"If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to login as any tenant administrator. " + "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to login as any tenant administrator. " +
"If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to login as any customer user. ") "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to login as any customer user. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) @GetMapping(value = "/user/tokenAccessEnabled")
@ResponseBody
public boolean isUserTokenAccessEnabled() { public boolean isUserTokenAccessEnabled() {
return userTokenAccessEnabled; return userTokenAccessEnabled;
} }
@ -161,8 +157,7 @@ public class UserController extends BaseController {
"If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to get the token of any tenant administrator. " + "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to get the token of any tenant administrator. " +
"If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to get the token of any customer user that belongs to the same tenant. ") "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to get the token of any customer user that belongs to the same tenant. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) @GetMapping(value = "/user/{userId}/token")
@ResponseBody
public JwtPair getUserToken( public JwtPair getUserToken(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId) throws ThingsboardException { @PathVariable(USER_ID) String strUserId) throws ThingsboardException {
@ -189,8 +184,7 @@ public class UserController extends BaseController {
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new User entity." + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new User entity." +
"\n\nAvailable for users with 'SYS_ADMIN', 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.") "\n\nAvailable for users with 'SYS_ADMIN', 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user", method = RequestMethod.POST) @PostMapping(value = "/user")
@ResponseBody
public User saveUser( public User saveUser(
@Parameter(description = "A JSON value representing the User.", required = true) @Parameter(description = "A JSON value representing the User.", required = true)
@RequestBody User user, @RequestBody User user,
@ -206,7 +200,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Send or re-send the activation email", @ApiOperation(value = "Send or re-send the activation email",
notes = "Force send the activation email to the user. Useful to resend the email if user has accidentally deleted it. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Force send the activation email to the user. Useful to resend the email if user has accidentally deleted it. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/sendActivationMail", method = RequestMethod.POST) @PostMapping(value = "/user/sendActivationMail")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void sendActivationEmail( public void sendActivationEmail(
@Parameter(description = "Email of the user", required = true) @Parameter(description = "Email of the user", required = true)
@ -229,7 +223,6 @@ public class UserController extends BaseController {
"The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) "The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain") @GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain")
@ResponseBody
public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION) public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId, @PathVariable(USER_ID) String strUserId,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
@ -255,7 +248,7 @@ public class UserController extends BaseController {
notes = "Deletes the User, it's credentials and all the relations (from and to the User). " + notes = "Deletes the User, it's credentials and all the relations (from and to the User). " +
"Referencing non-existing User Id will cause an error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) "Referencing non-existing User Id will cause an error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/user/{userId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteUser( public void deleteUser(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@ -276,8 +269,7 @@ public class UserController extends BaseController {
notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/users", params = {"pageSize", "page"})
@ResponseBody
public PageData<User> getUsers( public PageData<User> getUsers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -302,8 +294,7 @@ public class UserController extends BaseController {
notes = "Returns page of user data objects. Search is been executed by email, firstName and " + notes = "Returns page of user data objects. Search is been executed by email, firstName and " +
"lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users/info", method = RequestMethod.GET) @GetMapping(value = "/users/info")
@ResponseBody
public PageData<UserEmailInfo> findUsersByQuery( public PageData<UserEmailInfo> findUsersByQuery(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -339,8 +330,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Tenant Users (getTenantAdmins)", @ApiOperation(value = "Get Tenant Users (getTenantAdmins)",
notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"})
@ResponseBody
public PageData<User> getTenantAdmins( public PageData<User> getTenantAdmins(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId, @PathVariable(TENANT_ID) String strTenantId,
@ -363,8 +353,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Customer Users (getCustomerUsers)", @ApiOperation(value = "Get Customer Users (getCustomerUsers)",
notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"})
@ResponseBody
public PageData<User> getCustomerUsers( public PageData<User> getCustomerUsers(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId, @PathVariable(CUSTOMER_ID) String strCustomerId,
@ -389,8 +378,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Enable/Disable User credentials (setUserCredentialsEnabled)", @ApiOperation(value = "Enable/Disable User credentials (setUserCredentialsEnabled)",
notes = "Enables or Disables user credentials. Useful when you would like to block user account without deleting it. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Enables or Disables user credentials. Useful when you would like to block user account without deleting it. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST) @PostMapping(value = "/user/{userId}/userCredentialsEnabled")
@ResponseBody
public void setUserCredentialsEnabled( public void setUserCredentialsEnabled(
@Parameter(description = USER_ID_PARAM_DESCRIPTION) @Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId, @PathVariable(USER_ID) String strUserId,
@ -398,7 +386,7 @@ public class UserController extends BaseController {
@RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
checkParameter(USER_ID, strUserId); checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId)); UserId userId = new UserId(toUUID(strUserId));
User user = checkUserId(userId, Operation.WRITE); checkUserId(userId, Operation.WRITE);
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
@ -412,8 +400,7 @@ public class UserController extends BaseController {
"Search is been executed by email, firstName and lastName fields. " + "Search is been executed by email, firstName and lastName fields. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"})
@ResponseBody
public PageData<UserEmailInfo> getUsersForAssign( public PageData<UserEmailInfo> getUsersForAssign(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("alarmId") String strAlarmId, @PathVariable("alarmId") String strAlarmId,
@ -491,7 +478,7 @@ public class UserController extends BaseController {
notes = "Delete user settings by specifying list of json element xpaths. \n " + notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE) @DeleteMapping(value = "/user/settings/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS) public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException { @PathVariable(PATHS) String paths) throws ThingsboardException {
checkParameter(USER_ID, paths); checkParameter(USER_ID, paths);
@ -531,7 +518,7 @@ public class UserController extends BaseController {
notes = "Delete user settings by specifying list of json element xpaths. \n " + notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/settings/{type}/{paths}", method = RequestMethod.DELETE) @DeleteMapping(value = "/user/settings/{type}/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS) public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths, @PathVariable(PATHS) String paths,
@Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".")
@ -555,8 +542,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)", @ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)",
notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/dashboards/{dashboardId}/{action}", method = RequestMethod.GET) @GetMapping(value = "/user/dashboards/{dashboardId}/{action}")
@ResponseBody
public UserDashboardsInfo reportUserDashboardAction( public UserDashboardsInfo reportUserDashboardAction(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DashboardController.DASHBOARD_ID) String strDashboardId, @PathVariable(DashboardController.DASHBOARD_ID) String strDashboardId,

42
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -21,13 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -111,8 +111,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)",
notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeInfo/{widgetTypeId}", method = RequestMethod.GET) @GetMapping(value = "/widgetTypeInfo/{widgetTypeId}")
@ResponseBody
public WidgetTypeInfo getWidgetTypeInfoById( public WidgetTypeInfo getWidgetTypeInfoById(
@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
@ -132,8 +131,7 @@ public class WidgetTypeController extends AutoCommitController {
"Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." + "Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType", method = RequestMethod.POST) @PostMapping(value = "/widgetType")
@ResponseBody
public WidgetTypeDetails saveWidgetType( public WidgetTypeDetails saveWidgetType(
@Parameter(description = "A JSON value representing the Widget Type Details.", required = true) @Parameter(description = "A JSON value representing the Widget Type Details.", required = true)
@RequestBody WidgetTypeDetails widgetTypeDetails, @RequestBody WidgetTypeDetails widgetTypeDetails,
@ -153,7 +151,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Delete widget type (deleteWidgetType)", @ApiOperation(value = "Delete widget type (deleteWidgetType)",
notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/widgetType/{widgetTypeId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteWidgetType( public void deleteWidgetType(
@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@ -168,8 +166,7 @@ public class WidgetTypeController extends AutoCommitController {
notes = "Returns a page of Widget Type objects available for current user. " + WIDGET_TYPE_DESCRIPTION + " " + notes = "Returns a page of Widget Type objects available for current user. " + WIDGET_TYPE_DESCRIPTION + " " +
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypes", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypes", params = {"pageSize", "page"})
@ResponseBody
public PageData<WidgetTypeInfo> getWidgetTypes( public PageData<WidgetTypeInfo> getWidgetTypes(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -215,8 +212,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)",
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"})
@ResponseBody
@Deprecated @Deprecated
public List<WidgetType> getBundleWidgetTypesByBundleAlias( public List<WidgetType> getBundleWidgetTypesByBundleAlias(
@Parameter(description = "System or Tenant", required = true) @Parameter(description = "System or Tenant", required = true)
@ -236,8 +232,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)",
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"})
@ResponseBody
public List<WidgetType> getBundleWidgetTypes( public List<WidgetType> getBundleWidgetTypes(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@ -248,8 +243,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)",
notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"})
@ResponseBody
@Deprecated @Deprecated
public List<WidgetTypeDetails> getBundleWidgetTypesDetailsByBundleAlias( public List<WidgetTypeDetails> getBundleWidgetTypesDetailsByBundleAlias(
@Parameter(description = "System or Tenant", required = true) @Parameter(description = "System or Tenant", required = true)
@ -269,8 +263,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)", @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetails)",
notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"})
@ResponseBody
public List<WidgetTypeDetails> getBundleWidgetTypesDetails( public List<WidgetTypeDetails> getBundleWidgetTypesDetails(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId, @RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ -291,8 +284,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",
notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"})
@ResponseBody
public List<String> getBundleWidgetTypeFqns( public List<String> getBundleWidgetTypeFqns(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@ -303,8 +295,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"})
@ResponseBody
@Deprecated @Deprecated
public List<WidgetTypeInfo> getBundleWidgetTypesInfosByBundleAlias( public List<WidgetTypeInfo> getBundleWidgetTypesInfosByBundleAlias(
@Parameter(description = "System or Tenant", required = true) @Parameter(description = "System or Tenant", required = true)
@ -325,8 +316,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)", @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"})
@ResponseBody
public PageData<WidgetTypeInfo> getBundleWidgetTypesInfos( public PageData<WidgetTypeInfo> getBundleWidgetTypesInfos(
@Parameter(description = "Widget Bundle Id", required = true) @Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId, @RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ -357,8 +347,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", @ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)",
notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET) @GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"})
@ResponseBody
@Deprecated @Deprecated
public WidgetType getWidgetTypeByBundleAliasAndTypeAlias( public WidgetType getWidgetTypeByBundleAliasAndTypeAlias(
@Parameter(description = "System or Tenant", required = true) @Parameter(description = "System or Tenant", required = true)
@ -382,8 +371,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type (getWidgetType)", @ApiOperation(value = "Get Widget Type (getWidgetType)",
notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER, hidden = true) notes = "Get the Widget Type by FQN. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER, hidden = true)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET) @GetMapping(value = "/widgetType", params = {"fqn"})
@ResponseBody
public WidgetType getWidgetType( public WidgetType getWidgetType(
@Parameter(description = "Widget Type fqn", required = true) @Parameter(description = "Widget Type fqn", required = true)
@RequestParam String fqn) throws ThingsboardException { @RequestParam String fqn) throws ThingsboardException {

23
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -20,12 +20,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -79,8 +80,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)", @ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)",
notes = "Get the Widget Bundle based on the provided Widget Bundle Id. " + WIDGET_BUNDLE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Get the Widget Bundle based on the provided Widget Bundle Id. " + WIDGET_BUNDLE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET) @GetMapping(value = "/widgetsBundle/{widgetsBundleId}")
@ResponseBody
public WidgetsBundle getWidgetsBundleById( public WidgetsBundle getWidgetsBundleById(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetsBundleId") String strWidgetsBundleId, @PathVariable("widgetsBundleId") String strWidgetsBundleId,
@ -106,8 +106,7 @@ public class WidgetsBundleController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Widgets Bundle entity." + "Remove 'id', 'tenantId' from the request body example (below) to create new Widgets Bundle entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle", method = RequestMethod.POST) @PostMapping(value = "/widgetsBundle")
@ResponseBody
public WidgetsBundle saveWidgetsBundle( public WidgetsBundle saveWidgetsBundle(
@Parameter(description = "A JSON value representing the Widget Bundle.", required = true) @Parameter(description = "A JSON value representing the Widget Bundle.", required = true)
@RequestBody WidgetsBundle widgetsBundle) throws Exception { @RequestBody WidgetsBundle widgetsBundle) throws Exception {
@ -126,7 +125,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Update widgets bundle widgets types list (updateWidgetsBundleWidgetTypes)", @ApiOperation(value = "Update widgets bundle widgets types list (updateWidgetsBundleWidgetTypes)",
notes = "Updates widgets bundle widgets list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Updates widgets bundle widgets list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes", method = RequestMethod.POST) @PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void updateWidgetsBundleWidgetTypes( public void updateWidgetsBundleWidgetTypes(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@ -152,7 +151,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Update widgets bundle widgets list from widget type FQNs list (updateWidgetsBundleWidgetFqns)", @ApiOperation(value = "Update widgets bundle widgets list from widget type FQNs list (updateWidgetsBundleWidgetFqns)",
notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns", method = RequestMethod.POST) @PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void updateWidgetsBundleWidgetFqns( public void updateWidgetsBundleWidgetFqns(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@ -169,7 +168,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)", @ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)",
notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/widgetsBundle/{widgetsBundleId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteWidgetsBundle( public void deleteWidgetsBundle(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@ -184,8 +183,7 @@ public class WidgetsBundleController extends BaseController {
notes = "Returns a page of Widget Bundle objects available for current user. " + WIDGET_BUNDLE_DESCRIPTION + " " + notes = "Returns a page of Widget Bundle objects available for current user. " + WIDGET_BUNDLE_DESCRIPTION + " " +
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/widgetsBundles", params = {"pageSize", "page"})
@ResponseBody
public PageData<WidgetsBundle> getWidgetsBundles( public PageData<WidgetsBundle> getWidgetsBundles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -223,8 +221,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)",
notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET) @GetMapping(value = "/widgetsBundles")
@ResponseBody
public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException { public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId()));

4
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -174,9 +174,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
return future.get(); return future.get();
} catch (ExecutionException e) { } catch (ExecutionException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
throw new RuntimeException("Failed to fetch " + key + ": " + cause.getMessage(), cause); throw new RuntimeException("Failed to fetch '" + key + "' argument: " + cause.getMessage(), cause);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException("Failed to fetch" + key, e); throw new RuntimeException("Failed to fetch '" + key + "' argument!", e);
} }
} }

13
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -25,6 +26,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import org.thingsboard.server.utils.CalculatedFieldUtils; import org.thingsboard.server.utils.CalculatedFieldUtils;
import java.io.Closeable; import java.io.Closeable;
@ -33,6 +36,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Getter @Getter
public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable {
@ -164,6 +168,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
.mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L) .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L)
.max() .max()
.orElse(0L); .orElse(0L);
} else if (entry instanceof GeofencingArgumentEntry geofencingArgumentEntry) {
newTs = geofencingArgumentEntry.getZoneStates().values().stream()
.mapToLong(GeofencingZoneState::getTs).max().orElse(0L);
} }
this.latestTimestamp = Math.max(this.latestTimestamp, newTs); this.latestTimestamp = Math.max(this.latestTimestamp, newTs);
} }
@ -185,4 +192,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return ReadinessStatus.from(emptyArguments); return ReadinessStatus.from(emptyArguments);
} }
@Override
public JsonNode getArgumentsJson() {
return JacksonUtil.valueToTree(arguments.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())));
}
} }

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -649,7 +649,9 @@ public class CalculatedFieldCtx implements Closeable {
} }
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()))) { && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
|| !thisConfig.getMetrics().equals(otherConfig.getMetrics())
|| thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) {
return true; return true;
} }
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig

19
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -38,6 +39,7 @@ import java.io.Closeable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@ -59,6 +61,8 @@ public interface CalculatedFieldState extends Closeable {
Map<String, ArgumentEntry> getArguments(); Map<String, ArgumentEntry> getArguments();
JsonNode getArgumentsJson();
long getLatestTimestamp(); long getLatestTimestamp();
void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx);
@ -102,14 +106,25 @@ public interface CalculatedFieldState extends Closeable {
record ReadinessStatus(boolean ready, String errorMsg) { record ReadinessStatus(boolean ready, String errorMsg) {
private static final String ERROR_MESSAGE = "Required arguments are missing: "; private static final String MISSING_REQUIRED_ARGUMENTS_ERROR = "Required arguments are missing: ";
private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " +
"Verify the configured relation type and direction.";
private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: ";
private static final ReadinessStatus READY = new ReadinessStatus(true, null); private static final ReadinessStatus READY = new ReadinessStatus(true, null);
public static ReadinessStatus from(List<String> emptyOrMissingArguments) { public static ReadinessStatus from(List<String> emptyOrMissingArguments) {
if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) {
return ReadinessStatus.READY; return ReadinessStatus.READY;
} }
return new ReadinessStatus(false, ERROR_MESSAGE + String.join(", ", emptyOrMissingArguments)); boolean propagationCtxIsEmpty = emptyOrMissingArguments.remove(PROPAGATION_CONFIG_ARGUMENT);
if (!propagationCtxIsEmpty) {
return new ReadinessStatus(false, MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments));
}
if (emptyOrMissingArguments.isEmpty()) {
return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_ERROR);
}
return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR +
String.join(", ", emptyOrMissingArguments));
} }
} }

28
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.aggregation; package org.thingsboard.server.service.cf.ctx.state.aggregation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -23,6 +24,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput;
@ -31,9 +33,11 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInp
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry;
@ -44,6 +48,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -62,6 +67,8 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
private ScheduledFuture<?> reevaluationFuture; private ScheduledFuture<?> reevaluationFuture;
private EntityService entityService;
public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) {
super(entityId); super(entityId);
} }
@ -72,6 +79,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
metrics = configuration.getMetrics(); metrics = configuration.getMetrics();
deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec());
entityService = ctx.getSystemContext().getEntityService();
} }
@Override @Override
@ -247,4 +255,24 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
} }
} }
@Override
public JsonNode getArgumentsJson() {
Map<EntityId, Map<String, ArgumentEntry>> inputs = prepareInputs();
Map<EntityId, EntityInfo> entityIdEntityInfos = entityService.fetchEntityInfos(ctx.getTenantId(), null, inputs.keySet());
List<EntityArgument> entitiesArguments = new ArrayList<>();
inputs.forEach((entityId, entityArguments) -> {
EntityInfo entityInfo = entityIdEntityInfos.get(entityId);
if (entityInfo != null) {
JsonNode entityArgumentsJson = JacksonUtil.valueToTree(entityArguments.entrySet().stream()
.collect(Collectors.toMap(Entry::getKey, e -> e.getValue().jsonValue())));
entitiesArguments.add(new EntityArgument(entityInfo, entityArgumentsJson));
}
});
return JacksonUtil.valueToTree(new RelatedEntitiesArgument(ArgumentEntryType.RELATED_ENTITIES, entitiesArguments));
}
record RelatedEntitiesArgument(ArgumentEntryType type, List<EntityArgument> entitiesArguments) {}
record EntityArgument(EntityInfo entity, JsonNode entityArguments) {}
} }

100
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java

@ -15,12 +15,16 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.aggregation.single; package org.thingsboard.server.service.cf.ctx.state.aggregation.single;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
@ -36,6 +40,7 @@ import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
@ -58,6 +63,8 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
private long checkInterval; private long checkInterval;
private Map<String, AggMetric> metrics; private Map<String, AggMetric> metrics;
private EntityAggregationDebugArgumentsTracker debugTracker;
private CalculatedFieldProcessingService cfProcessingService; private CalculatedFieldProcessingService cfProcessingService;
public EntityAggregationCalculatedFieldState(EntityId entityId) { public EntityAggregationCalculatedFieldState(EntityId entityId) {
@ -94,6 +101,15 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
createIntervalIfNotExist(); createIntervalIfNotExist();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
if (debugTracker == null) {
debugTracker = new EntityAggregationDebugArgumentsTracker(new HashMap<>());
} else {
debugTracker.reset();
}
debugTracker.recordUpdatedArgs(updatedArgs, arguments);
}
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>(); Map<AggIntervalEntry, Map<String, ArgumentEntry>> results = new HashMap<>();
List<AggIntervalEntry> expiredIntervals = new ArrayList<>(); List<AggIntervalEntry> expiredIntervals = new ArrayList<>();
getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { getIntervals().forEach((intervalEntry, argIntervalStatuses) -> {
@ -114,6 +130,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
.build()); .build());
} }
@Override
public Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> argumentValues, CalculatedFieldCtx ctx) {
createIntervalIfNotExist();
return super.update(argumentValues, ctx);
}
private void removeExpiredIntervals(List<AggIntervalEntry> expiredIntervals) { private void removeExpiredIntervals(List<AggIntervalEntry> expiredIntervals) {
expiredIntervals.forEach(expiredInterval -> { expiredIntervals.forEach(expiredInterval -> {
arguments.values().stream() arguments.values().stream()
@ -183,6 +205,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
expiredIntervals.add(intervalEntry); expiredIntervals.add(intervalEntry);
} else if (now - startTs >= intervalEntry.getIntervalDuration()) { } else if (now - startTs >= intervalEntry.getIntervalDuration()) {
handleActiveInterval(intervalEntry, args, results); handleActiveInterval(intervalEntry, args, results);
if (watermarkDuration == 0) {
expiredIntervals.add(intervalEntry);
}
} }
} }
@ -262,14 +287,89 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
resultNode.put("ts", interval.getEndTs() - 1); resultNode.put("ts", interval.getEndTs() - 1);
resultNode.set("values", metricsNode); resultNode.set("values", metricsNode);
result.add(resultNode); result.add(resultNode);
if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) {
if (debugTracker != null) {
debugTracker.addInterval(interval);
}
}
} }
}); });
return result; return result;
} }
@Override
public JsonNode getArgumentsJson() {
if (debugTracker == null) {
return null;
}
EntityAggregationDebugArguments debugArguments = debugTracker.toDebugArguments();
return debugArguments == null ? null : JacksonUtil.valueToTree(debugArguments);
}
@Override @Override
public boolean isReady() { public boolean isReady() {
return true; return true;
} }
record EntityAggregationDebugArgumentsTracker(Map<AggIntervalEntry, Map<String, TbelCfArg>> processedIntervals) {
public void reset() {
processedIntervals.clear();
}
public void addInterval(AggIntervalEntry interval) {
processedIntervals.computeIfAbsent(interval, k -> new HashMap<>());
}
public void recordUpdatedArgs(Map<String, ArgumentEntry> updatedArgs, Map<String, ArgumentEntry> arguments) {
if (updatedArgs != null && !updatedArgs.isEmpty()) {
updatedArgs.forEach((argName, argEntry) -> {
ArgumentEntry argumentEntry = arguments.get(argName);
if (argumentEntry instanceof EntityAggregationArgumentEntry entityAggEntry && argEntry instanceof SingleValueArgumentEntry singleEntry) {
entityAggEntry.getAggIntervals().forEach((aggIntervalEntry, aggIntervalEntryStatus) -> {
boolean match = singleEntry.isForceResetPrevious() || aggIntervalEntry.belongsToInterval(singleEntry.getTs());
if (match) {
recordArg(aggIntervalEntry, argName, singleEntry.toTbelCfArg());
}
});
}
});
}
}
public void recordArg(AggIntervalEntry interval, String argName, TbelCfArg value) {
processedIntervals.computeIfAbsent(interval, k -> new HashMap<>()).put(argName, value);
}
public EntityAggregationDebugArguments toDebugArguments() {
if (processedIntervals.isEmpty()) {
return null;
}
return EntityAggregationDebugArguments.toDebugArguments(processedIntervals);
}
}
record EntityAggregationDebugArguments(List<IntervalDebugArgument> processedIntervals) {
public static EntityAggregationDebugArguments toDebugArguments(Map<AggIntervalEntry, Map<String, TbelCfArg>> processedIntervals) {
List<IntervalDebugArgument> result = new ArrayList<>();
processedIntervals.forEach((interval, args) -> {
result.add(new IntervalDebugArgument(interval.getStartTs(), interval.getEndTs(), args));
});
return new EntityAggregationDebugArguments(result);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
record IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, JsonNode updatedArguments) {
public IntervalDebugArgument(Long intervalStartTs, Long intervalEndTs, Map<String, TbelCfArg> updatedArguments) {
this(intervalStartTs, intervalEndTs, updatedArguments == null || updatedArguments.isEmpty() ? null : JacksonUtil.valueToTree(updatedArguments));
}
}
} }

31
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -107,23 +107,26 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones(); boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones();
List<GeofencingEvalResult> zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size()); List<GeofencingEvalResult> zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size());
argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> {
boolean firstEval = zoneState.getLastPresence() == null;
GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates);
zoneResults.add(eval); zoneResults.add(eval);
if (createRelationsWithMatchedZones) { if (!createRelationsWithMatchedZones) {
GeofencingTransitionEvent transitionEvent = eval.transition(); return;
if (transitionEvent == null) {
return;
}
EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType());
};
ListenableFuture<Boolean> f = switch (transitionEvent) {
case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation);
case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation);
};
relationFutures.add(f);
} }
GeofencingTransitionEvent transitionEvent = eval.transition();
if (transitionEvent == null && !firstEval) {
return;
}
transitionEvent = transitionEvent == null ? GeofencingTransitionEvent.LEFT : transitionEvent;
EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType());
};
ListenableFuture<Boolean> f = switch (transitionEvent) {
case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation);
case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation);
};
relationFutures.add(f);
}); });
updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode); updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
}); });

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java

@ -46,10 +46,13 @@ public class GeofencingZoneState {
public GeofencingZoneState(EntityId zoneId, KvEntry entry) { public GeofencingZoneState(EntityId zoneId, KvEntry entry) {
this.zoneId = zoneId; this.zoneId = zoneId;
if (!(entry instanceof AttributeKvEntry attributeKvEntry)) { if (!(entry instanceof AttributeKvEntry attributeKvEntry)) {
throw new IllegalArgumentException("Unsupported KvEntry type for geofencing zone state: " + entry.getClass().getSimpleName()); throw new IllegalArgumentException("Invalid perimeter data source for zone with id: " + zoneId + ". Perimeter definition must be stored as attribute!");
} }
this.ts = attributeKvEntry.getLastUpdateTs(); this.ts = attributeKvEntry.getLastUpdateTs();
this.version = attributeKvEntry.getVersion(); this.version = attributeKvEntry.getVersion();
if (entry.getValueAsString() == null) {
throw new IllegalArgumentException("Perimeter attribute key '" + entry.getKey() + "' not found for Zone with id: " + zoneId);
}
this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class); this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class);
} }

5
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -75,6 +75,7 @@ import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgePr
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor;
import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService;
import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService;
@ -265,9 +266,13 @@ public class EdgeContextComponent {
@Autowired @Autowired
private AiModelService aiModelService; private AiModelService aiModelService;
@Autowired @Autowired
private AiModelProcessor aiModelProcessor; private AiModelProcessor aiModelProcessor;
@Autowired
private UserProcessor userProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType); EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) { if (processor == null) {

24
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -84,6 +84,8 @@ import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg;
import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -101,6 +103,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -122,6 +125,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private final EdgeSessionState sessionState = new EdgeSessionState(); private final EdgeSessionState sessionState = new EdgeSessionState();
private final ReentrantLock downlinkMsgLock = new ReentrantLock(); private final ReentrantLock downlinkMsgLock = new ReentrantLock();
private final Lock sequenceDependencyLock = new ReentrantLock();
protected EdgeContextComponent ctx; protected EdgeContextComponent ctx;
protected Edge edge; protected Edge edge;
@ -940,6 +944,26 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
} }
} }
if (uplinkMsg.getUserUpdateMsgCount() > 0) {
for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) {
for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

155
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2025 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.edge.rpc.processor.user;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
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.id.UserId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseUserProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<User> userValidator;
protected Pair<Boolean, Boolean> saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg) {
boolean isCreated = false;
boolean userEmailUpdated = false;
try {
User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
if (user == null) {
throw new IllegalArgumentException(String.format("[%s] Failed to parse User from UserUpdateMsg: %s", tenantId, userUpdateMsg));
}
User userById = edgeCtx.getUserService().findUserById(tenantId, userId);
if (userById == null) {
isCreated = true;
user.setId(null);
} else {
user.setId(userId);
}
String userEmail = user.getEmail();
User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail());
if (existing != null && !existing.getId().equals(user.getId())) {
String[] splitEmail = userEmail.split("@");
userEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1];
log.warn("[{}] User with email {} already exists. Renaming User email to {}",
tenantId, user.getEmail(), userEmail);
userEmailUpdated = true;
}
user.setEmail(userEmail);
setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg);
userValidator.validate(user, User::getTenantId);
if (isCreated) {
user.setId(userId);
}
edgeCtx.getUserService().saveUser(tenantId, user, false);
} catch (Exception e) {
log.error("[{}] Failed to process user update msg [{}]", tenantId, userUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, userEmailUpdated);
}
protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId) {
deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, null);
}
protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) {
User removedUser = deleteUser(tenantId, userId);
if (removedUser == null) {
return;
}
CustomerId userCustomerId = removedUser.getCustomerId();
String userAsString = JacksonUtil.toString(removedUser);
TbMsgMetaData msgMetaData = edge == null ? new TbMsgMetaData() : getEdgeActionTbMsgMetaData(edge, userCustomerId);
addRemovedUserMetadata(msgMetaData, removedUser);
pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData);
}
private User deleteUser(TenantId tenantId, UserId userId) {
User userById = edgeCtx.getUserService().findUserById(tenantId, userId);
if (userById == null) {
log.trace("[{}] User with id {} does not exist", tenantId, userId);
return null;
}
edgeCtx.getUserService().deleteUser(tenantId, userById);
return userById;
}
protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) {
UserCredentials userCredentials = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true);
if (userCredentials == null) {
throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg));
}
User user = edgeCtx.getUserService().findUserById(tenantId, userCredentials.getUserId());
if (user == null) {
log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]",
tenantId, userCredentials.getUserId(), updateMsg);
return;
}
log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]",
tenantId, user.getName(), userCredentials.getId(), userCredentials.isEnabled());
try {
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId());
if (userCredentialsByUserId != null && !userCredentialsByUserId.getId().equals(userCredentials.getId())) {
edgeCtx.getUserService().deleteUserCredentials(tenantId, userCredentialsByUserId);
}
edgeCtx.getUserService().saveUserCredentials(tenantId, userCredentials, false);
} catch (Exception e) {
log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]",
tenantId, user.getName(), updateMsg, e);
throw new RuntimeException(e);
}
}
private void addRemovedUserMetadata(TbMsgMetaData metaData, User removedUser) {
metaData.putValue("userId", removedUser.getId().toString());
metaData.putValue("userName", removedUser.getName());
metaData.putValue("userEmail", removedUser.getEmail());
if (removedUser.getFirstName() != null) {
metaData.putValue("userFirstName", removedUser.getFirstName());
}
if (removedUser.getLastName() != null) {
metaData.putValue("userLastName", removedUser.getLastName());
}
}
protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg);
}

101
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -15,26 +15,110 @@
*/ */
package org.thingsboard.server.service.edge.rpc.processor.user; package org.thingsboard.server.service.edge.rpc.processor.user;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Slf4j @Slf4j
@Component @Component
@TbCoreComponent @TbCoreComponent
public class UserEdgeProcessor extends BaseEdgeProcessor { public class UserEdgeProcessor extends BaseUserProcessor implements UserProcessor {
@Override
public ListenableFuture<Void> processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg) {
log.trace("[{}] executing processUserMsgFromEdge [{}] from edge [{}]", tenantId, userUpdateMsg, edge.getId());
UserId userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
return switch (userUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
saveOrUpdateUser(tenantId, userId, userUpdateMsg, edge);
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, edge);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(userUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed users violated {}", tenantId, userUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public ListenableFuture<Void> processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg) {
log.debug("[{}] Executing processUserCredentialsMsgFromEdge, userCredentialsUpdateMsg [{}]", tenantId, userCredentialsUpdateMsg);
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
super.updateUserCredentials(tenantId, userCredentialsUpdateMsg);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
return Futures.immediateFuture(null);
}
private void saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateUser(tenantId, userId, userUpdateMsg);
boolean isCreated = resultPair.getFirst();
if (isCreated) {
createRelationFromEdge(tenantId, edge.getId(), userId);
pushUserCreatedEventToRuleEngine(tenantId, edge, userId);
}
boolean userEmailUpdated = resultPair.getSecond();
if (userEmailUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null);
}
}
private void pushUserCreatedEventToRuleEngine(TenantId tenantId, Edge edge, UserId userId) {
try {
User user = edgeCtx.getUserService().findUserById(tenantId, userId);
if (user != null) {
String userAsString = JacksonUtil.toString(user);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, user.getCustomerId());
pushEntityEventToRuleEngine(tenantId, userId, user.getCustomerId(), TbMsgType.ENTITY_CREATED, userAsString, msgMetaData);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to push user action to rule engine: {}", tenantId, userId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
@Override @Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
@ -48,7 +132,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user)); .addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user));
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId));
} }
return builder.build(); return builder.build();
@ -62,11 +146,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
} }
case CREDENTIALS_UPDATED -> { case CREDENTIALS_UPDATED -> {
UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { if (userCredentialsByUserId != null) {
UserCredentialsUpdateMsg userCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId);
return DownlinkMsg.newBuilder() return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) .addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId))
.build(); .build();
} }
} }
@ -79,4 +162,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
return EdgeEventType.USER; return EdgeEventType.USER;
} }
@Override
protected void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg) {
CustomerId customerUUID = user.getCustomerId() != null ? user.getCustomerId() : customerId;
user.setCustomerId(customerUUID);
}
} }

31
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2025 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.edge.rpc.processor.user;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface UserProcessor extends EdgeProcessor {
ListenableFuture<Void> processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg);
ListenableFuture<Void> processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg);
}

95
application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2025 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.security.auth;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserAuthDetails;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractAuthenticationProvider implements AuthenticationProvider {
private final CustomerService customerService;
private final UserAuthDetailsCache userAuthDetailsCache;
protected SecurityUser authenticateByPublicId(String publicId, String authContextName, UserPrincipal userPrincipal) {
TenantId systemId = TenantId.SYS_TENANT_ID;
CustomerId customerId;
try {
customerId = new CustomerId(UUID.fromString(publicId));
} catch (Exception e) {
throw new BadCredentialsException(authContextName + " is not valid");
}
Customer publicCustomer = customerService.findCustomerById(systemId, customerId);
if (publicCustomer == null) {
throw new UsernameNotFoundException("Public entity not found");
}
if (!publicCustomer.isPublic()) {
throw new BadCredentialsException(authContextName + " is not valid");
}
User user = new User(new UserId(EntityId.NULL_UUID));
user.setTenantId(publicCustomer.getTenantId());
user.setCustomerId(publicCustomer.getId());
user.setEmail(publicId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName("Public");
user.setLastName("Public");
UserPrincipal principal = userPrincipal == null ? new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId) : userPrincipal;
return new SecurityUser(user, true, principal);
}
protected SecurityUser authenticateByUserId(TenantId tenantId, UserId userId) {
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(tenantId, userId);
if (userAuthDetails == null) {
throw new UsernameNotFoundException("User with credentials not found");
}
if (!userAuthDetails.credentialsEnabled()) {
throw new DisabledException("User is not active");
}
User user = userAuthDetails.user();
if (user.getAuthority() == null) {
throw new InsufficientAuthenticationException("User has no authority assigned");
}
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
return new SecurityUser(user, true, userPrincipal);
}
}

76
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java

@ -15,26 +15,14 @@
*/ */
package org.thingsboard.server.service.security.auth.jwt; package org.thingsboard.server.service.security.auth.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserAuthDetails;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.service.security.auth.AbstractAuthenticationProvider;
import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -43,17 +31,19 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; import org.thingsboard.server.service.user.cache.UserAuthDetailsCache;
import java.util.UUID;
@Component @Component
@RequiredArgsConstructor public class RefreshTokenAuthenticationProvider extends AbstractAuthenticationProvider {
public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final UserAuthDetailsCache userAuthDetailsCache;
private final CustomerService customerService;
private final TokenOutdatingService tokenOutdatingService; private final TokenOutdatingService tokenOutdatingService;
public RefreshTokenAuthenticationProvider(JwtTokenFactory jwtTokenFactory, UserAuthDetailsCache userAuthDetailsCache,
CustomerService customerService, TokenOutdatingService tokenOutdatingService) {
super(customerService, userAuthDetailsCache);
this.tokenFactory = jwtTokenFactory;
this.tokenOutdatingService = tokenOutdatingService;
}
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided"); Assert.notNull(authentication, "No authentication data provided");
@ -63,7 +53,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
SecurityUser securityUser; SecurityUser securityUser;
if (principal.getType() == UserPrincipal.Type.USER_NAME) { if (principal.getType() == UserPrincipal.Type.USER_NAME) {
securityUser = authenticateByUserId(unsafeUser.getId()); securityUser = authenticateByUserId(TenantId.SYS_TENANT_ID, unsafeUser.getId());
} else { } else {
securityUser = authenticateByPublicId(principal.getValue()); securityUser = authenticateByPublicId(principal.getValue());
} }
@ -75,52 +65,8 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
return new RefreshAuthenticationToken(securityUser); return new RefreshAuthenticationToken(securityUser);
} }
private SecurityUser authenticateByUserId(UserId userId) {
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(TenantId.SYS_TENANT_ID, userId);
if (userAuthDetails == null) {
throw new UsernameNotFoundException("User with credentials not found");
}
if (!userAuthDetails.credentialsEnabled()) {
throw new DisabledException("User is not active");
}
User user = userAuthDetails.user();
if (user.getAuthority() == null) {
throw new InsufficientAuthenticationException("User has no authority assigned");
}
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
return new SecurityUser(user, true, userPrincipal);
}
private SecurityUser authenticateByPublicId(String publicId) { private SecurityUser authenticateByPublicId(String publicId) {
TenantId systemId = TenantId.SYS_TENANT_ID; return super.authenticateByPublicId(publicId, "Refresh token", null);
CustomerId customerId;
try {
customerId = new CustomerId(UUID.fromString(publicId));
} catch (Exception e) {
throw new BadCredentialsException("Refresh token is not valid");
}
Customer publicCustomer = customerService.findCustomerById(systemId, customerId);
if (publicCustomer == null) {
throw new UsernameNotFoundException("Public entity not found by refresh token");
}
if (!publicCustomer.isPublic()) {
throw new BadCredentialsException("Refresh token is not valid");
}
User user = new User(new UserId(EntityId.NULL_UUID));
user.setTenantId(publicCustomer.getTenantId());
user.setCustomerId(publicCustomer.getId());
user.setEmail(publicId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName("Public");
user.setLastName("Public");
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId);
return new SecurityUser(user, true, userPrincipal);
} }
@Override @Override

45
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java

@ -15,42 +15,35 @@
*/ */
package org.thingsboard.server.service.security.auth.pat; package org.thingsboard.server.service.security.auth.pat;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserAuthDetails;
import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.service.security.auth.AbstractAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest;
import org.thingsboard.server.service.security.model.token.RawApiKey;
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; import org.thingsboard.server.service.user.cache.UserAuthDetailsCache;
@Component @Component
@RequiredArgsConstructor public class ApiKeyAuthenticationProvider extends AbstractAuthenticationProvider {
public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
private final ApiKeyService apiKeyService; private final ApiKeyService apiKeyService;
private final UserAuthDetailsCache userAuthDetailsCache;
@Override public ApiKeyAuthenticationProvider(ApiKeyService apiKeyService, UserAuthDetailsCache userAuthDetailsCache) {
public Authentication authenticate(Authentication authentication) throws AuthenticationException { super(null, userAuthDetailsCache);
RawApiKey rawApiKey = (RawApiKey) authentication.getCredentials(); this.apiKeyService = apiKeyService;
SecurityUser securityUser = authenticate(rawApiKey.apiKey());
return new ApiKeyAuthenticationToken(securityUser);
} }
@Override @Override
public boolean supports(Class<?> authentication) { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication); ApiKeyAuthRequest apiKeyAuthRequest = (ApiKeyAuthRequest) authentication.getCredentials();
SecurityUser securityUser = authenticate(apiKeyAuthRequest.apiKey());
return new ApiKeyAuthenticationToken(securityUser);
} }
private SecurityUser authenticate(String key) { private SecurityUser authenticate(String key) {
@ -67,20 +60,12 @@ public class ApiKeyAuthenticationProvider implements org.springframework.securit
if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) { if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) {
throw new CredentialsExpiredException("API key is expired"); throw new CredentialsExpiredException("API key is expired");
} }
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(apiKey.getTenantId(), apiKey.getUserId()); return super.authenticateByUserId(apiKey.getTenantId(), apiKey.getUserId());
if (userAuthDetails == null) { }
throw new UsernameNotFoundException("User with credentials not found");
}
if (!userAuthDetails.credentialsEnabled()) {
throw new DisabledException("User is not active");
}
User user = userAuthDetails.user(); @Override
if (user.getAuthority() == null) { public boolean supports(Class<?> authentication) {
throw new InsufficientAuthenticationException("User has no authority assigned"); return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
}
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
return new SecurityUser(user, true, userPrincipal);
} }
} }

12
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java

@ -17,7 +17,7 @@ package org.thingsboard.server.service.security.auth.pat;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.RawApiKey; import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest;
import java.io.Serial; import java.io.Serial;
@ -26,12 +26,12 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Serial @Serial
private static final long serialVersionUID = 2978710889397403536L; private static final long serialVersionUID = 2978710889397403536L;
private RawApiKey rawApiKey; private ApiKeyAuthRequest apiKeyAuthRequest;
private SecurityUser securityUser; private SecurityUser securityUser;
public ApiKeyAuthenticationToken(RawApiKey rawApiKey) { public ApiKeyAuthenticationToken(ApiKeyAuthRequest apiKeyAuthRequest) {
super(null); super(null);
this.rawApiKey = rawApiKey; this.apiKeyAuthRequest = apiKeyAuthRequest;
setAuthenticated(false); setAuthenticated(false);
} }
@ -44,7 +44,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public Object getCredentials() { public Object getCredentials() {
return rawApiKey; return apiKeyAuthRequest;
} }
@Override @Override
@ -55,7 +55,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public void eraseCredentials() { public void eraseCredentials() {
super.eraseCredentials(); super.eraseCredentials();
this.rawApiKey = null; this.apiKeyAuthRequest = null;
} }
} }

7
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java

@ -29,7 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; import org.thingsboard.server.service.security.auth.extractor.TokenExtractor;
import org.thingsboard.server.service.security.model.token.RawApiKey; import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest;
import java.io.IOException; import java.io.IOException;
@ -52,8 +52,9 @@ public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthentic
@Override @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
RawApiKey rawApiKey = new RawApiKey(tokenExtractor.extract(request)); String apiKeyValue = tokenExtractor.extract(request);
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(rawApiKey)); ApiKeyAuthRequest apiKeyAuthRequest = new ApiKeyAuthRequest(apiKeyValue);
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(apiKeyAuthRequest));
} }
@Override @Override

42
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.rest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
@ -27,14 +26,9 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
@ -43,6 +37,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.AbstractAuthenticationProvider;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaConfigurationToken; import org.thingsboard.server.service.security.auth.MfaConfigurationToken;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
@ -51,18 +46,14 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.UUID;
@Component @Component
@Slf4j @Slf4j
@TbCoreComponent @TbCoreComponent
public class RestAuthenticationProvider implements AuthenticationProvider { public class RestAuthenticationProvider extends AbstractAuthenticationProvider {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService; private final SecuritySettingsService securitySettingsService;
private final UserService userService; private final UserService userService;
private final CustomerService customerService;
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
@Autowired @Autowired
@ -71,8 +62,8 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
final SystemSecurityService systemSecurityService, final SystemSecurityService systemSecurityService,
SecuritySettingsService securitySettingsService, SecuritySettingsService securitySettingsService,
TwoFactorAuthService twoFactorAuthService) { TwoFactorAuthService twoFactorAuthService) {
super(customerService, null);
this.userService = userService; this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService; this.systemSecurityService = systemSecurityService;
this.securitySettingsService = securitySettingsService; this.securitySettingsService = securitySettingsService;
this.twoFactorAuthService = twoFactorAuthService; this.twoFactorAuthService = twoFactorAuthService;
@ -125,7 +116,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
} }
try { try {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
if (userCredentials == null) { if (userCredentials == null) {
throw new UsernameNotFoundException("User credentials not found"); throw new UsernameNotFoundException("User credentials not found");
@ -138,8 +128,9 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
throw e; throw e;
} }
if (user.getAuthority() == null) if (user.getAuthority() == null) {
throw new InsufficientAuthenticationException("User has no authority assigned"); throw new InsufficientAuthenticationException("User has no authority assigned");
}
return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
} catch (Exception e) { } catch (Exception e) {
@ -149,28 +140,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
} }
private SecurityUser authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { private SecurityUser authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
CustomerId customerId; return super.authenticateByPublicId(publicId, "Public Id", userPrincipal);
try {
customerId = new CustomerId(UUID.fromString(publicId));
} catch (Exception e) {
throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
}
Customer publicCustomer = customerService.findCustomerById(TenantId.SYS_TENANT_ID, customerId);
if (publicCustomer == null) {
throw new UsernameNotFoundException("Public entity not found: " + publicId);
}
if (!publicCustomer.isPublic()) {
throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
}
User user = new User(new UserId(EntityId.NULL_UUID));
user.setTenantId(publicCustomer.getTenantId());
user.setCustomerId(publicCustomer.getId());
user.setEmail(publicId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName("Public");
user.setLastName("Public");
return new SecurityUser(user, true, userPrincipal);
} }
@Override @Override

2
application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKey.java → application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java

@ -15,4 +15,4 @@
*/ */
package org.thingsboard.server.service.security.model.token; package org.thingsboard.server.service.security.model.token;
public record RawApiKey(String apiKey) {} public record ApiKeyAuthRequest(String apiKey) {}

12
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -24,8 +24,8 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
@ -96,15 +96,15 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
private RuleEngineStatisticsService ruleEngineStatisticsService; private RuleEngineStatisticsService ruleEngineStatisticsService;
@Autowired @Autowired
private StatsFactory statsFactory; private StatsFactory statsFactory;
@SpyBean
private TimeseriesDao timeseriesDao;
@Autowired @Autowired
private QueueStatsService queueStatsService; private QueueStatsService queueStatsService;
@SpyBean @MockitoSpyBean
private TimeseriesDao timeseriesDao;
@MockitoSpyBean
private PartitionService partitionService; private PartitionService partitionService;
@SpyBean @MockitoSpyBean
private TimeseriesService timeseriesService; private TimeseriesService timeseriesService;
@SpyBean @MockitoSpyBean
private ActorSystemContext actorSystemContext; private ActorSystemContext actorSystemContext;
@Test @Test

2
application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java

@ -189,7 +189,7 @@ public class ImageControllerTest extends AbstractControllerTest {
assertThat(newImageInfo.getTitle()).isEqualTo(newTitle); assertThat(newImageInfo.getTitle()).isEqualTo(newTitle);
assertThat(newImageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor); assertThat(newImageInfo.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor);
assertThat(newImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey()); assertThat(newImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(newImageInfo.getPublicResourceKey()); assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(imageInfo.getPublicResourceKey());
} }
@Test @Test

14
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -27,8 +27,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
@ -109,11 +109,11 @@ public class TenantControllerTest extends AbstractControllerTest {
ListeningExecutorService executor; ListeningExecutorService executor;
@SpyBean @MockitoSpyBean
private PartitionService partitionService; private PartitionService partitionService;
@SpyBean @MockitoSpyBean
private ActorSystemContext actorContext; private ActorSystemContext actorContext;
@SpyBean @MockitoSpyBean
private TbQueueAdmin queueAdmin; private TbQueueAdmin queueAdmin;
@Before @Before
@ -269,12 +269,11 @@ public class TenantControllerTest extends AbstractControllerTest {
@Test @Test
public void testFindTenants() throws Exception { public void testFindTenants() throws Exception {
loginSysAdmin(); loginSysAdmin();
List<Tenant> tenants = new ArrayList<>();
PageLink pageLink = new PageLink(17); PageLink pageLink = new PageLink(17);
PageData<Tenant> pageData = doGetTypedWithPageLink("/api/tenants?", PAGE_DATA_TENANT_TYPE_REF, pageLink); PageData<Tenant> pageData = doGetTypedWithPageLink("/api/tenants?", PAGE_DATA_TENANT_TYPE_REF, pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
tenants.addAll(pageData.getData()); List<Tenant> tenants = new ArrayList<>(pageData.getData());
Mockito.reset(tbClusterService); Mockito.reset(tbClusterService);
@ -400,12 +399,11 @@ public class TenantControllerTest extends AbstractControllerTest {
@Test @Test
public void testFindTenantInfos() throws Exception { public void testFindTenantInfos() throws Exception {
loginSysAdmin(); loginSysAdmin();
List<TenantInfo> tenants = new ArrayList<>();
PageLink pageLink = new PageLink(17); PageLink pageLink = new PageLink(17);
PageData<TenantInfo> pageData = doGetTypedWithPageLink("/api/tenantInfos?", PAGE_DATA_TENANT_INFO_TYPE_REF, pageLink); PageData<TenantInfo> pageData = doGetTypedWithPageLink("/api/tenantInfos?", PAGE_DATA_TENANT_INFO_TYPE_REF, pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
tenants.addAll(pageData.getData()); List<TenantInfo> tenants = new ArrayList<>(pageData.getData());
List<ListenableFuture<TenantInfo>> createFutures = new ArrayList<>(56); List<ListenableFuture<TenantInfo>> createFutures = new ArrayList<>(56);
for (int i = 0; i < 56; i++) { for (int i = 0; i < 56; i++) {

393
application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java

@ -15,15 +15,22 @@
*/ */
package org.thingsboard.server.edge; package org.thingsboard.server.edge;
import com.google.protobuf.AbstractMessage; import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.web.servlet.ResultMatcher;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -32,11 +39,15 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH;
@DaoSqlTest @DaoSqlTest
public class UserEdgeTest extends AbstractEdgeTest { public class UserEdgeTest extends AbstractEdgeTest {
@ -44,216 +55,304 @@ public class UserEdgeTest extends AbstractEdgeTest {
@Autowired @Autowired
private BCryptPasswordEncoder passwordEncoder; private BCryptPasswordEncoder passwordEncoder;
private static final String DEFAULT_FIRST_NAME = "Boris";
private static final String DEFAULT_LAST_NAME = "Johnson";
private static final String UPDATED_LAST_NAME = "Borisov";
private static final String DEFAULT_TENANT_ADMIN_EMAIL = "tenantAdmin@thingsboard.org";
private static final String DEFAULT_CUSTOMER_USER_EMAIL = "customerUser@thingsboard.org";
@Test @Test
public void testCreateUpdateDeleteTenantUser() throws Exception { public void testCreateUpdateDeleteTenantUser() throws Exception {
// create user // create user
edgeImitator.expectMessageAmount(3); User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null);
User newTenantAdmin = new User(); User savedTenantAdmin = createAndVerifyUserOnEdge(newTenantAdmin);
newTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
newTenantAdmin.setTenantId(tenantId);
newTenantAdmin.setEmail("tenantAdmin@thingsboard.org");
newTenantAdmin.setFirstName("Boris");
newTenantAdmin.setLastName("Johnson");
User savedTenantAdmin = createUser(newTenantAdmin, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedTenantAdmin.getId(), userMsg.getId());
Assert.assertEquals(savedTenantAdmin.getAuthority(), userMsg.getAuthority());
Assert.assertEquals(savedTenantAdmin.getEmail(), userMsg.getEmail());
Assert.assertEquals(savedTenantAdmin.getFirstName(), userMsg.getFirstName());
Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName());
Optional<UserCredentialsUpdateMsg> userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class);
Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent());
// update user // update user
edgeImitator.expectMessageAmount(2); updateAndVerifyUserLastName(savedTenantAdmin);
savedTenantAdmin.setLastName("Borisov");
savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName());
// update user credentials // update user credentials
login(savedTenantAdmin.getEmail(), "tenant"); login(savedTenantAdmin.getEmail(), "tenant");
updateAndVerifyUserCredentials(savedTenantAdmin);
loginTenantAdmin();
edgeImitator.expectMessageAmount(1); // delete user
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); deleteAndVerifyUser(savedTenantAdmin);
changePasswordRequest.setCurrentPassword("tenant"); }
changePasswordRequest.setNewPassword("newTenant");
doPost("/api/auth/changePassword", changePasswordRequest); @Test
Assert.assertTrue(edgeImitator.waitForMessages()); public void testCreateUpdateDeleteCustomerUser() throws Exception {
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); // create customer
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Customer savedCustomer = createAndAssignCustomerToEdge();
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedTenantAdmin.getId(), userCredentialsMsg.getUserId());
Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword()));
// create user
User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId());
User savedCustomerUser = createAndVerifyUserOnEdge(customerUser);
// update user
updateAndVerifyUserLastName(savedCustomerUser);
// update user credentials
login(savedCustomerUser.getEmail(), "customer");
updateAndVerifyUserCredentials(savedCustomerUser);
loginTenantAdmin(); loginTenantAdmin();
// delete user // delete user
deleteAndVerifyUser(savedCustomerUser);
}
@Test
public void testSendUserToCloudFromEdge() throws Exception {
// create customer
Customer savedCustomer = createAndAssignCustomerToEdge();
// create uplinkMsg with user and userCredentials
UserId userId = new UserId(UUID.randomUUID());
UserCredentialsId userCredentialsId = new UserCredentialsId(UUID.randomUUID());
UplinkMsg uplinkMsg = buildUserUplinkMsg(userId, savedCustomer.getId(), userCredentialsId);
User userFromCloud = verifyMsgOnCloud(uplinkMsg, userId, false);
assertUserCredentialsFlags(userFromCloud, false, false);
// create uplinkMsg with enabled userCredentials
UplinkMsg uplinkMsgWithEnabledCredentials = constructUserCredentialsUplinkMsg(userCredentialsId, userId);
User cloudUserWithCredentials = verifyMsgOnCloud(uplinkMsgWithEnabledCredentials, userId, false);
assertUserCredentialsFlags(cloudUserWithCredentials, true, true);
// create uplinkMsg with user the same email
UserId secondUserId = new UserId(UUID.randomUUID());
UserCredentialsId secondCredentialsId = new UserCredentialsId(UUID.randomUUID());
UplinkMsg uplinkMsgForUserExistingEmail = buildUserUplinkMsg(secondUserId, savedCustomer.getId(), secondCredentialsId);
verifyMsgOnCloud(uplinkMsgForUserExistingEmail, secondUserId, true);
}
@Test
public void testSendUserCredentialsRequestToCloud() throws Exception {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder();
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doDelete("/api/user/" + savedTenantAdmin.getUuidId()) edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
.andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = getLatestUserCredentialsUpdateMsg();
userUpdateMsg = (UserUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId());
Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB());
testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg);
} }
@Test @Test
public void testCreateUpdateDeleteCustomerUser() throws Exception { public void testSendUserDeleteFromEdgeToCloud() throws Exception {
// create customer // create customer
Customer savedCustomer = createAndAssignCustomerToEdge();
// create user
User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId());
User savedCustomerUser = createAndVerifyUserOnEdge(customerUser);
// simulate user removal event from edge to cloud
UserUpdateMsg.Builder userUpdateMsg = UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits())
.setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits());
UplinkMsg uplink = UplinkMsg.newBuilder()
.setUplinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(userUpdateMsg).build();
testAutoGeneratedCodeByProtobuf(userUpdateMsg);
// expect edge message sent & cloud message response
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplink);
Assert.assertTrue(edgeImitator.waitForResponses());
loginTenantAdmin();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.until(() -> {
try {
doGet("/api/user/" + savedCustomerUser.getId(), User.class, status().isNotFound());
return true;
} catch (Throwable ex) {
return false;
}
});
}
private Customer createAndAssignCustomerToEdge() throws Exception {
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTitle("Edge Customer"); customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class); Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
Assert.assertFalse(edgeImitator.waitForMessages(5)); Assert.assertFalse(edgeImitator.waitForMessages(5));
// assign edge to customer
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class);
+ "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// create user return savedCustomer;
}
private User buildUser(Authority authority, CustomerId customerId) {
User user = new User();
user.setAuthority(authority);
user.setTenantId(tenantId);
user.setCustomerId(customerId);
user.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL);
user.setFirstName(DEFAULT_FIRST_NAME);
user.setLastName(DEFAULT_LAST_NAME);
return user;
}
private User createAndVerifyUserOnEdge(User user) throws Exception {
String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer";
// wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
edgeImitator.expectMessageAmount(3); edgeImitator.expectMessageAmount(3);
User customerUser = new User(); User savedUser = createUser(user, password);
customerUser.setAuthority(Authority.CUSTOMER_USER); Assert.assertTrue(edgeImitator.waitForMessages());
customerUser.setTenantId(tenantId);
customerUser.setCustomerId(savedCustomer.getId());
customerUser.setEmail("customerUser@thingsboard.org");
customerUser.setFirstName("John");
customerUser.setLastName("Edwards");
User savedCustomerUser = createUser(customerUser, "customer");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg); Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getId(), userMsg.getId()); Assert.assertEquals(savedUser.getId(), userMsg.getId());
Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); Assert.assertEquals(savedUser.getCustomerId(), userMsg.getCustomerId());
Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); Assert.assertEquals(savedUser.getAuthority(), userMsg.getAuthority());
Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); Assert.assertEquals(savedUser.getEmail(), userMsg.getEmail());
Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); Assert.assertEquals(savedUser.getFirstName(), userMsg.getFirstName());
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); Assert.assertEquals(savedUser.getLastName(), userMsg.getLastName());
return savedUser;
}
private void updateAndVerifyUserLastName(User user) throws Exception {
user.setLastName(UPDATED_LAST_NAME);
// update user
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(2);
savedCustomerUser.setLastName("Addams"); doPost("/api/user", user, User.class);
savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
userUpdateMsg = userUpdateMsgOpt.get(); User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userFromMsg);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName());
}
// update user credentials private void updateAndVerifyUserCredentials(User user) throws Exception {
login(savedCustomerUser.getEmail(), "customer"); String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer";
String newPassword = "new" + password;
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest();
changePasswordRequest.setCurrentPassword("customer"); changePasswordRequest.setCurrentPassword(password);
changePasswordRequest.setNewPassword("newCustomer"); changePasswordRequest.setNewPassword(newPassword);
doPost("/api/auth/changePassword", changePasswordRequest); doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(userCredentialsMsg);
Assert.assertEquals(savedCustomerUser.getId(), userCredentialsMsg.getUserId());
Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword()));
loginTenantAdmin(); UserCredentialsUpdateMsg msg = getLatestUserCredentialsUpdateMsg();
UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true);
Assert.assertNotNull(creds);
Assert.assertEquals(user.getId(), creds.getUserId());
Assert.assertTrue(passwordEncoder.matches(newPassword, creds.getPassword()));
}
// delete user private void deleteAndVerifyUser(User savedTenantAdmin) throws Exception {
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doDelete("/api/user/" + savedCustomerUser.getUuidId()) doDelete("/api/user/" + savedTenantAdmin.getUuidId())
.andExpect(status().isOk()); .andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg();
userUpdateMsg = (UserUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB());
Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB());
}
private UplinkMsg buildUserUplinkMsg(UserId userId, CustomerId customerId, UserCredentialsId userCredentialsUuid) {
User customerUser = buildUser(Authority.CUSTOMER_USER, customerId);
customerUser.setId(userId);
UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, false);
userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUser);
UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials);
return UplinkMsg.newBuilder()
.addUserUpdateMsg(userUpdateMsg)
.addUserCredentialsUpdateMsg(userCredentialsMsg)
.build();
} }
@Test private UserCredentials buildCredentials(UserCredentialsId userCredentialsUuid, UserId userId, boolean enabled) {
public void testSendUserCredentialsRequestToCloud() throws Exception { UserCredentials userCredentials = new UserCredentials();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder();
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); userCredentials.setId(userCredentialsUuid);
userCredentials.setUserId(userId);
userCredentials.setEnabled(enabled);
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
return userCredentials;
}
private User verifyMsgOnCloud(UplinkMsg uplinkMsg, UserId userId, boolean emailExist) throws Exception {
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsg);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); User userFromCloud = doGet("/api/user/" + userId, User.class);
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Assert.assertNotNull(userFromCloud);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); if (emailExist) {
Assert.assertNotNull(userCredentialsMsg); Assert.assertNotEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail());
Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); } else {
Assert.assertEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail());
}
return userFromCloud;
} }
@Test private UplinkMsg constructUserCredentialsUplinkMsg(UserCredentialsId userCredentialsUuid, UserId userId) {
public void sendUserCredentialsRequest() throws Exception { UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, true);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); userCredentials.setPassword("password");
UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials);
userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits());
userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder);
uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); return UplinkMsg.newBuilder()
.addUserCredentialsUpdateMsg(credsMsg)
.build();
}
edgeImitator.expectResponsesAmount(1); private void assertUserCredentialsFlags(User user, boolean enabled, boolean activated) {
edgeImitator.expectMessageAmount(1); JsonNode info = user.getAdditionalInfo();
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); Assert.assertNotNull(info);
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertEquals(enabled, info.get("userCredentialsEnabled").asBoolean());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertEquals(activated, info.get("userActivated").asBoolean());
}
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); private UserUpdateMsg getLatestUserUpdateMsg() {
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Optional<UserUpdateMsg> opt = edgeImitator.findMessageByType(UserUpdateMsg.class);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; Assert.assertTrue(opt.isPresent());
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); return opt.get();
Assert.assertNotNull(userCredentialsMsg); }
Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId());
testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); private UserCredentialsUpdateMsg getLatestUserCredentialsUpdateMsg() {
Optional<UserCredentialsUpdateMsg> opt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class);
Assert.assertTrue(opt.isPresent());
return opt.get();
} }
} }

47
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -221,7 +221,7 @@ public class GeofencingCalculatedFieldStateTest {
ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
"allowedZones", geofencingAllowedZoneArgEntry, "allowedZones", geofencingAllowedZoneArgEntry,
"restrictedZones", new GeofencingArgumentEntry() "restrictedZones", new GeofencingArgumentEntry(Collections.emptyMap())
), ctx); ), ctx);
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones");
@ -290,10 +290,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0);
assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID);
assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1);
assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID);
assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId());
} }
@Test @Test
@ -360,10 +367,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0);
assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID);
assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1);
assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID);
assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId());
} }
@Test @Test
@ -432,10 +446,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0);
assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID);
assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId());
EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1);
assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID);
assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId());
} }
private CalculatedField getCalculatedField() { private CalculatedField getCalculatedField() {

33
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
@ -52,10 +54,12 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalcul
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -126,21 +130,28 @@ public class PropagationCalculatedFieldStateTest {
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
} }
@Test private static Stream<ArgumentEntry> provideInvalidPropagationArgs() {
void testIsReadyWhenPropagationArgIsNull() { return Stream.of(
initCtxAndState(false); null,
state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); new PropagationArgumentEntry(Collections.emptyList())
assertThat(state.isReady()).isFalse(); );
assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT);
} }
@Test @ParameterizedTest
void testIsReadyWhenPropagationArgIsEmpty() { @MethodSource("provideInvalidPropagationArgs")
void testIsReadyWhenPropagationArgIsNullOrEmpty(ArgumentEntry propagationEntry) {
initCtxAndState(false); initCtxAndState(false);
state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry,
PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); Map<String, ArgumentEntry> args = new HashMap<>();
args.put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); // Valid user arg
if (propagationEntry != null) {
args.put(PROPAGATION_CONFIG_ARGUMENT, propagationEntry);
}
state.update(args, ctx);
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); assertThat(state.getReadinessStatus().errorMsg())
.isEqualTo("No entities found via 'Propagation path to related entities'. Verify the configured relation type and direction.");
} }
@Test @Test

4
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -24,8 +24,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -125,7 +125,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
private NotificationCenter notificationCenter; private NotificationCenter notificationCenter;
@Autowired @Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@MockBean @MockitoBean
private FirebaseService firebaseService; private FirebaseService firebaseService;
private static final String TEST_MOBILE_TOKEN = "tenantFcmToken"; private static final String TEST_MOBILE_TOKEN = "tenantFcmToken";

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

@ -21,11 +21,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable; import org.junit.function.ThrowingRunnable;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.SystemUtil;
import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -108,6 +110,7 @@ import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -120,6 +123,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset; import static org.assertj.core.api.Assertions.offset;
import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mockStatic;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED; import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED;
import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.UNASSIGNED; import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.UNASSIGNED;
@ -796,9 +800,14 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId()); createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin(); loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); // Mock SystemUtil to return 15% memory usage (exceeds 1% threshold)
method.setAccessible(true); try (MockedStatic<SystemUtil> mockedSystemUtil = mockStatic(SystemUtil.class)) {
method.invoke(systemInfoService); mockedSystemUtil.when(SystemUtil::getMemoryUsage).thenReturn(Optional.of(15));
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
}
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1); await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0); Notification notification = getMyNotifications(false, 100).get(0);
@ -846,16 +855,24 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception { public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception {
loginSysAdmin(); loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder() ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(1f) .ramThreshold(0.99f)
.cpuThreshold(1f) .cpuThreshold(0.99f)
.storageThreshold(1f) .storageThreshold(0.99f)
.build(); .build();
NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId()); NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin(); loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); // Mock SystemUtil to return 15% usages (not exceeds 99% threshold)
method.setAccessible(true); Method method;
method.invoke(systemInfoService); try (MockedStatic<SystemUtil> mockedSystemUtil = mockStatic(SystemUtil.class)) {
mockedSystemUtil.when(SystemUtil::getMemoryUsage).thenReturn(Optional.of(15));
mockedSystemUtil.when(SystemUtil::getCpuUsage).thenReturn(Optional.of(15));
mockedSystemUtil.when(SystemUtil::getDiscSpaceUsage).thenReturn(Optional.of(15));
method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
}
TimeUnit.SECONDS.sleep(5); TimeUnit.SECONDS.sleep(5);
assertThat(getMyNotifications(false, 100)).size().isZero(); assertThat(getMyNotifications(false, 100)).size().isZero();

2
application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java

@ -107,7 +107,7 @@ class CalculatedFieldUtilsTest {
assertThat(fromProto) assertThat(fromProto)
.usingRecursiveComparison() .usingRecursiveComparison()
.ignoringFields("ctx", "requiredArguments", "readinessStatus") .ignoringFields("ctx", "requiredArguments", "readinessStatus", "latestTimestamp")
.isEqualTo(state); .isEqualTo(state);
ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest");

6
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -48,6 +48,8 @@ public interface UserService extends EntityDaoService {
User saveUser(TenantId tenantId, User user); User saveUser(TenantId tenantId, User user);
User saveUser(TenantId tenantId, User user, boolean doValidate);
UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId);
UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken);
@ -56,6 +58,8 @@ public interface UserService extends EntityDaoService {
UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials);
UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate);
UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password);
UserCredentials requestPasswordReset(TenantId tenantId, String email); UserCredentials requestPasswordReset(TenantId tenantId, String email);
@ -70,6 +74,8 @@ public interface UserService extends EntityDaoService {
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, User user); void deleteUser(TenantId tenantId, User user);
PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink); PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink);

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

@ -105,6 +105,8 @@ public class DataConstants {
public static final String RPC_FAILED = "RPC_FAILED"; public static final String RPC_FAILED = "RPC_FAILED";
public static final String RPC_DELETED = "RPC_DELETED"; public static final String RPC_DELETED = "RPC_DELETED";
public static final String REEVALUATION_MSG = "REEVALUATION_MSG";
public static final String DEFAULT_SECRET_KEY = ""; public static final String DEFAULT_SECRET_KEY = "";
public static final String SECRET_KEY_FIELD_NAME = "secretKey"; public static final String SECRET_KEY_FIELD_NAME = "secretKey";
public static final String DURATION_MS_FIELD_NAME = "durationMs"; public static final String DURATION_MS_FIELD_NAME = "durationMs";

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoNullChar;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial; import java.io.Serial;
@ -64,6 +65,7 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
@NotBlank @NotBlank
@NoNullChar @NoNullChar
@Length(min = 1, max = 255) @Length(min = 1, max = 255)
@NoXss
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List; import java.util.List;
@ -42,6 +43,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo<OAuth2ClientId> imp
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "Oauth2 client title") @Schema(description = "Oauth2 client title")
@NotBlank @NotBlank
@NoXss
@Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars")
private String title; private String title;
@Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED)

2
common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java

@ -32,7 +32,7 @@ public class ApiKey extends ApiKeyInfo {
private static final long serialVersionUID = -2313196723950490263L; private static final long serialVersionUID = -2313196723950490263L;
@NoXss @NoXss
@Schema(description = "Api key value", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "API key value", requiredMode = Schema.RequiredMode.REQUIRED)
private String value; private String value;
public ApiKey() { public ApiKey() {

20
common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java

@ -38,26 +38,26 @@ public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId {
@Serial @Serial
private static final long serialVersionUID = -2313196723950490263L; private static final long serialVersionUID = -2313196723950490263L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the api key cannot be changed.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with Tenant Id. Tenant Id of the API key cannot be changed.", accessMode = Schema.AccessMode.READ_ONLY)
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "JSON object with User Id. User Id of the api key cannot be changed.") @Schema(description = "JSON object with User Id. User Id of the API key cannot be changed.")
private UserId userId; private UserId userId;
@Schema(description = "Expiration time of the api key.") @Schema(description = "Expiration time of the API key.")
private long expirationTime; private long expirationTime;
@NoXss @NoXss
@NotBlank @NotBlank
@Length(fieldName = "description") @Length(fieldName = "description")
@Schema(description = "Api Key description.", example = "Api Key description") @Schema(description = "API Key description.", example = "API Key description")
private String description; private String description;
@Schema(description = "Enabled/disabled api key.", example = "true") @Schema(description = "Enabled/disabled API key.", example = "true")
private boolean enabled; private boolean enabled;
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@Schema(description = "Indicates if the api key is expired based on current time. Returns false if expirationTime is 0 (no expiry).", @Schema(description = "Indicates if the API key is expired based on current time. Returns false if expirationTime is 0 (no expiry).",
example = "false", example = "false",
accessMode = Schema.AccessMode.READ_ONLY) accessMode = Schema.AccessMode.READ_ONLY)
public boolean isExpired() { public boolean isExpired() {
@ -67,10 +67,10 @@ public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId {
return System.currentTimeMillis() > expirationTime; return System.currentTimeMillis() > expirationTime;
} }
@Schema(description = "JSON object with the Api Key Id. " + @Schema(description = "JSON object with the API Key Id. " +
"Specify this field to update the Api Key. " + "Specify this field to update the API Key. " +
"Referencing non-existing Api Key Id will cause error. " + "Referencing non-existing API Key Id will cause error. " +
"Omit this field to create new Api Key.") "Omit this field to create new API Key.")
@Override @Override
public ApiKeyId getId() { public ApiKeyId getId() {
return super.getId(); return super.getId();

8
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -16,7 +16,7 @@
package org.thingsboard.server.common.data.tenant.profile; package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -174,12 +174,16 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxArgumentsPerCF = 10; private long maxArgumentsPerCF = 10;
@Schema(example = "60") @Schema(example = "60")
private int minAllowedScheduledUpdateIntervalInSecForCF = 60; private int minAllowedScheduledUpdateIntervalInSecForCF = 60;
@Builder.Default
@Schema(example = "10") @Schema(example = "10")
@Positive
private int maxRelationLevelPerCfArgument = 10; private int maxRelationLevelPerCfArgument = 10;
@Builder.Default
@Schema(example = "100") @Schema(example = "100")
@Positive
private int maxRelatedEntitiesToReturnPerCfArgument = 100; private int maxRelatedEntitiesToReturnPerCfArgument = 100;
@Builder.Default @Builder.Default
@Min(value = 1, message = "must be at least 1") @Positive
@Schema(example = "1000") @Schema(example = "1000")
private long maxDataPointsPerRollingArg = 1000; private long maxDataPointsPerRollingArg = 1000;
@Schema(example = "32") @Schema(example = "32")

2
common/edge-api/src/main/proto/edge.proto

@ -449,6 +449,8 @@ message UplinkMsg {
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
repeated AiModelUpdateMsg aiModelUpdateMsg = 27; repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
repeated UserUpdateMsg userUpdateMsg = 28;
repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29;
} }
message UplinkResponseMsg { message UplinkResponseMsg {

3
dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java

@ -36,9 +36,6 @@ import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Created by ashvayka on 13.07.17.
*/
@Data @Data
@MappedSuperclass @MappedSuperclass
public abstract class BaseSqlEntity<D> implements BaseEntity<D> { public abstract class BaseSqlEntity<D> implements BaseEntity<D> {

4
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java

@ -40,10 +40,6 @@ import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY;
/**
* Created by Victor Basanets on 8/30/2017.
*/
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@MappedSuperclass @MappedSuperclass

3
dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java

@ -33,9 +33,6 @@ import org.thingsboard.server.dao.model.ModelConstants;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 4/21/2017.
*/
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Entity @Entity

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java

@ -32,7 +32,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_MOBILE_APP_BUNDLE_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME;
@Data @Data
@Entity @Entity
@Table(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME) @Table(name = MOBILE_APP_BUNDLE_OAUTH2_CLIENT_TABLE_NAME)
@ -63,4 +62,5 @@ public final class MobileAppBundleOauth2ClientEntity implements ToData<MobileApp
result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId)); result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId));
return result; return result;
} }
} }

38
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -96,8 +96,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory"; public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
private static final int DEFAULT_TOKEN_LENGTH = 30; public static final int DEFAULT_TOKEN_LENGTH = 30;
public static final String INCORRECT_USER_ID = "Incorrect userId "; public static final String INCORRECT_USER_ID = "Incorrect userId ";
public static final String INCORRECT_USER_CREDENTIALS_ID = "Incorrect userCredentialsId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Value("${security.user_login_case_sensitive:true}") @Value("${security.user_login_case_sensitive:true}")
@ -173,12 +174,23 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override @Override
@Transactional @Transactional
public User saveUser(TenantId tenantId, User user) { public User saveUser(TenantId tenantId, User user) {
return saveEntity(user, () -> doSaveUser(tenantId, user)); return saveUser(tenantId, user, true);
} }
private User doSaveUser(TenantId tenantId, User user) { @Override
@Transactional
public User saveUser(TenantId tenantId, User user, boolean doValidate) {
return saveEntity(user, () -> doSaveUser(tenantId, user, doValidate));
}
private User doSaveUser(TenantId tenantId, User user, boolean doValidate) {
log.trace("Executing saveUser [{}]", user); log.trace("Executing saveUser [{}]", user);
User oldUser = userValidator.validate(user, User::getTenantId); User oldUser = null;
if (doValidate) {
oldUser = userValidator.validate(user, User::getTenantId);
} else if (user.getId() != null) {
oldUser = findUserById(user.getTenantId(), user.getId());
}
if (!userLoginCaseSensitive) { if (!userLoginCaseSensitive) {
user.setEmail(user.getEmail().toLowerCase()); user.setEmail(user.getEmail().toLowerCase());
} }
@ -233,8 +245,15 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override @Override
public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
return saveUserCredentials(tenantId, userCredentials, true);
}
@Override
public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate) {
log.trace("Executing saveUserCredentials [{}]", userCredentials); log.trace("Executing saveUserCredentials [{}]", userCredentials);
userCredentialsValidator.validate(userCredentials, data -> tenantId); if (doValidate) {
userCredentialsValidator.validate(userCredentials, data -> tenantId);
}
UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); UserCredentials result = userCredentialsDao.save(tenantId, userCredentials);
eventPublisher.publishEvent(ActionEntityEvent.builder() eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -337,6 +356,15 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return result; return result;
} }
@Override
public void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
Objects.requireNonNull(userCredentials, "UserCredentials is null");
UserCredentialsId userCredentialsId = userCredentials.getId();
log.trace("[{}] Executing deleteUserCredentials [{}]", tenantId, userCredentialsId);
validateId(userCredentialsId, id -> INCORRECT_USER_CREDENTIALS_ID + id);
userCredentialsDao.removeById(tenantId, userCredentialsId.getId());
}
@Override @Override
@Transactional @Transactional
public void deleteUser(TenantId tenantId, User user) { public void deleteUser(TenantId tenantId, User user) {

1
dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java

@ -42,6 +42,7 @@ public class ApiKeyServiceTest extends AbstractServiceTest {
@Autowired @Autowired
ApiKeyService apiKeyService; ApiKeyService apiKeyService;
@Autowired @Autowired
UserService userService; UserService userService;

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

@ -168,9 +168,8 @@ export class DashboardUtilsService {
dashboard.configuration.filters = {}; dashboard.configuration.filters = {};
} }
if (isUndefined(dashboard.configuration.timewindow)) { dashboard.configuration.timewindow = initModelFromDefaultTimewindow(dashboard.configuration.timewindow,
dashboard.configuration.timewindow = this.timeService.defaultTimewindow(true); false, false, this.timeService, true, true);
}
if (isUndefined(dashboard.configuration.settings)) { if (isUndefined(dashboard.configuration.settings)) {
dashboard.configuration.settings = {}; dashboard.configuration.settings = {};
dashboard.configuration.settings.stateControllerId = 'entity'; dashboard.configuration.settings.stateControllerId = 'entity';

12
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -58,6 +58,7 @@
<tb-entity-type-select appearance="outline" <tb-entity-type-select appearance="outline"
subscriptSizing="dynamic" subscriptSizing="dynamic"
showLabel showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}" label="{{ 'alarm-rule.target-entity-type' | translate }}"
[filterAllowedEntityTypes]="false" [filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList" [allowedEntityTypes]="alarmRuleEntityTypeList"
@ -83,12 +84,14 @@
[entityId]="data.entityId || fieldFormGroup.get('entityId').value" [entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[ownerId]="data.ownerId" [ownerId]="data.ownerId"
[watchKeyChange]="true"
[disabledAddButton]="!fieldFormGroup.get('entityId.id').value"
[entityName]="data.entityName"/> [entityName]="data.entityName"/>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments"> <tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments" [testScript]="onTestScript.bind(this)">
</tb-create-cf-alarm-rules> </tb-create-cf-alarm-rules>
</div> </div>
</div> </div>
@ -97,7 +100,7 @@
<div class="flex flex-row items-center justify-start gap-2 pb-2" <div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value"> [class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row"> <div class="clear-alarm-rule flex flex-1 flex-row">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments"> <tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments" [testScript]="onTestScript.bind(this)" isClearCondition>
</tb-cf-alarm-rule> </tb-cf-alarm-rule>
</div> </div>
<button mat-icon-button <button mat-icon-button
@ -116,10 +119,7 @@
<div [class.!hidden]="configFormGroup.get('clearRule').value"> <div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
type="button" type="button"
(click)="addClearAlarmRule()" (click)="addClearAlarmRule()">
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'alarm-rule.add-clear-alarm-rule' | translate }} {{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button> </button>
</div> </div>

37
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -31,8 +31,15 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips"; import { MatChipInputEvent } from "@angular/material/chips";
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; import {
AlarmRule,
AlarmRuleConditionType,
AlarmRuleExpressionType,
AlarmRuleTestScriptFn
} from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils"; import { deepTrim } from "@core/utils";
import { Observable } from "rxjs";
import { switchMap } from "rxjs/operators";
export interface AlarmRuleDialogData { export interface AlarmRuleDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -43,6 +50,7 @@ export interface AlarmRuleDialogData {
ownerId: EntityId; ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
isDirty?: boolean; isDirty?: boolean;
getTestScriptDialogFn: AlarmRuleTestScriptFn,
} }
@Component({ @Component({
@ -59,7 +67,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
debugSettings: [], debugSettings: [],
entityId: this.fb.group({ entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required), entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required),
id: ['', Validators.required], id: [null as null | string, Validators.required],
}), }),
configuration: this.fb.group({ configuration: this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}),
@ -93,7 +101,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeIsLoading();
this.applyDialogData(); this.applyDialogData();
} }
@ -170,16 +177,18 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false}); this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
} }
private observeIsLoading(): void { onTestScript(expression: string): Observable<string> {
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { const calculatedFieldId = this.data.value?.id?.id;
if (loading) { if (calculatedFieldId) {
this.fieldFormGroup.disable({emitEvent: false}); return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
} else { .pipe(
this.fieldFormGroup.enable({emitEvent: false}); switchMap(event => {
if (this.data.isDirty) { const args = event?.arguments ? JSON.parse(event.arguments) : null;
this.fieldFormGroup.markAsDirty(); return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, args, false);
} }),
} takeUntilDestroyed(this.destroyRef)
}); )
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
} }
} }

5
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts

@ -90,7 +90,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
panelMode = false; panelMode = false;
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter'); buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
alarmRuleFilterConfigForm: FormGroup; alarmRuleFilterConfigForm: FormGroup;
@ -281,6 +281,9 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
if (this.alarmRuleFilterConfig?.name?.length) { if (this.alarmRuleFilterConfig?.name?.length) {
filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', ')); filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', '));
} }
if (this.alarmRuleFilterConfig?.entityType) {
filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.alarmRuleFilterConfig.entityType).type));
}
if (!filterTextParts.length) { if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title'); this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
} else { } else {

1
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts

@ -18,7 +18,6 @@ import { Component } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; import { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/calculated-field.models";
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";

83
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -26,7 +26,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs'; import { EMPTY, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -36,13 +36,17 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators'; import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import { import {
ArgumentEntityType, ArgumentEntityType,
ArgumentType,
CalculatedField, CalculatedField,
CalculatedFieldAlarmRule, CalculatedFieldAlarmRule,
CalculatedFieldEventArguments,
CalculatedFieldsQuery, CalculatedFieldsQuery,
CalculatedFieldType, CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
@ -57,8 +61,13 @@ import {
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; } from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { deepClone, getEntityDetailsPageURL } from "@core/utils"; import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import { ActionNotificationShow } from "@core/notification/notification.actions";
import {
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestScriptDialogData
} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component";
export class AlarmRulesTableConfig extends EntityTableConfig<any> { export class AlarmRulesTableConfig extends EntityTableConfig<any> {
@ -143,7 +152,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.cellActionDescriptors.push( this.cellActionDescriptors.push(
{ {
name: this.translate.instant('notification.copy-template'), name: this.translate.instant('alarm-rule.copy'),
icon: 'content_copy', icon: 'content_copy',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.copyCalculatedField(entity) onAction: ($event, entity) => this.copyCalculatedField(entity)
@ -223,7 +232,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void { private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
const copyCalculatedAlarmRule = deepClone(calculatedField); const copyCalculatedAlarmRule = deepClone(calculatedField);
copyCalculatedAlarmRule.entityId = null; if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty) this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty)
.subscribe((res) => { .subscribe((res) => {
if (res) { if (res) {
@ -245,6 +257,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty, isDirty,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
}, },
enterAnimationDuration: isDirty ? 0 : null, enterAnimationDuration: isDirty ? 0 : null,
}) })
@ -277,6 +290,19 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.importExportService.openCalculatedFieldImportDialog() this.importExportService.openCalculatedFieldImportDialog()
.pipe( .pipe(
filter(Boolean), filter(Boolean),
switchMap(calculatedField => {
if (calculatedField.type !== CalculatedFieldType.ALARM) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('alarm-rule.import-invalid-alarm-rule-type'),
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left',
duration: 5000
}));
return EMPTY;
}
return of(calculatedField);
}),
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean), filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -287,15 +313,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
} }
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
if (calculatedField.type === CalculatedFieldType.GEOFENCING) { if (calculatedField.type === CalculatedFieldType.ALARM) {
calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => {
const arg = calculatedField.configuration.zoneGroups[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
} else {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key]; const arg = calculatedField.configuration.arguments[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
@ -315,4 +333,41 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedField, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
if (calculatedField.type === CalculatedFieldType.ALARM) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key)
? {...argumentsObj[key], type}
: type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()};
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
}
}),
);
} else {
return of(null);
}
}
} }

19
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -67,9 +67,26 @@
[helpPopupStyle]="{ width: '1200px' }" [helpPopupStyle]="{ width: '1200px' }"
helpId="alarm-rule/expression_fn"> helpId="alarm-rule/expression_fn">
<div toolbarPrefixButton <div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.script' | translate }} class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.tbel' | translate }}
</div> </div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!argumentsList.length"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func> </tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</div> </div>
} }
</div> </div>

12
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -28,6 +28,7 @@ import {
AlarmRuleCondition, AlarmRuleCondition,
AlarmRuleConditionType, AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap, AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType, AlarmRuleExpressionType,
AlarmRuleFilter AlarmRuleFilter
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
@ -39,11 +40,13 @@ import {
import { TbEditorCompleter } from "@shared/models/ace/completion.models"; import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models"; import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { Observable } from "rxjs";
export interface CfAlarmRuleConditionDialogData { export interface CfAlarmRuleConditionDialogData {
readonly: boolean; readonly: boolean;
condition: AlarmRuleCondition; condition: AlarmRuleCondition;
arguments?: Record<string, CalculatedFieldArgument>; arguments?: Record<string, CalculatedFieldArgument>;
testScript: (expression: string) => Observable<string>;
} }
@Component({ @Component({
@ -120,7 +123,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.patchValue({ this.conditionFormGroup.patchValue({
expression: { expression: {
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE, type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
expression: this.condition?.expression?.expression ?? null, expression: this.condition?.expression?.expression ?? alarmRuleDefaultScript,
filters: this.condition?.expression?.filters ?? [], filters: this.condition?.expression?.filters ?? [],
operation: this.condition?.expression?.operation ?? ComplexOperation.AND operation: this.condition?.expression?.operation ?? ComplexOperation.AND
}, },
@ -253,4 +256,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition); this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
} }
onTestScript() {
this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe(
(expression) => {
this.conditionFormGroup.get('expression.expression').setValue(expression);
this.conditionFormGroup.get('expression.expression').markAsDirty();
})
}
} }

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -30,7 +30,7 @@
[nowrap]="true" [nowrap]="true"
[specText]="specText" [specText]="specText"
required required
addFilterPrompt="{{'alarm-rule.enter-alarm-rule-condition-prompt' | translate}}"> addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text> </tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon> <mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
</div> </div>

30
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -20,9 +20,8 @@ import {
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl, ValidationErrors,
Validator, Validator
Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
@ -50,6 +49,7 @@ import {
CfAlarmScheduleDialogComponent CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion"; import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({ @Component({
selector: 'tb-cf-alarm-rule-condition', selector: 'tb-cf-alarm-rule-condition',
@ -81,6 +81,13 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmRuleConditionFormGroup = this.fb.group({ alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'], type: ['SIMPLE'],
expression: [{type: AlarmRuleExpressionType.SIMPLE}], expression: [{type: AlarmRuleExpressionType.SIMPLE}],
@ -118,17 +125,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
} }
writeValue(value: AlarmRuleCondition): void { writeValue(value: AlarmRuleCondition): void {
if (value) { this.modelValue = value;
this.modelValue = value; this.updateConditionInfo();
this.updateConditionInfo();
}
} }
public conditionSet() { public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters) || !this.required; return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return this.conditionSet() ? null : { return this.conditionSet() ? null : {
alarmRuleCondition: { alarmRuleCondition: {
valid: false, valid: false,
@ -147,7 +152,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
data: { data: {
readonly: this.disabled, readonly: this.disabled,
condition: this.disabled ? this.modelValue : deepClone(this.modelValue), condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
arguments: this.arguments arguments: this.arguments,
testScript: this.testScript
} }
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {
@ -160,11 +166,11 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateConditionInfo() { private updateConditionInfo() {
this.alarmRuleConditionFormGroup.patchValue( this.alarmRuleConditionFormGroup.patchValue(
{ this.modelValue ? {
type: this.modelValue?.type, type: this.modelValue?.type,
expression: this.modelValue?.expression, expression: this.modelValue?.expression,
schedule: this.modelValue?.schedule, schedule: this.modelValue?.schedule,
}, {emitEvent: false} } : null, {emitEvent: false}
); );
this.updateScheduleText(); this.updateScheduleText();
this.updateSpecText(); this.updateSpecText();

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -16,7 +16,7 @@
--> -->
<div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup"> <div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required"> <tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition> </tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">

1
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
width: 100%;
.tb-alarm-rule-details, .tb-alarm-rule-dashboard { .tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px; padding: 4px;
&.title { &.title {

31
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts

@ -20,8 +20,9 @@ import {
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl, ValidationErrors,
Validator Validator,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
@ -34,6 +35,7 @@ import {
AlarmRuleDetailsDialogData AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion"; import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({ @Component({
selector: 'tb-cf-alarm-rule', selector: 'tb-cf-alarm-rule',
@ -65,10 +67,17 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
private modelValue: AlarmRule; private modelValue: AlarmRule;
alarmRuleFormGroup = this.fb.group({ alarmRuleFormGroup = this.fb.group({
condition: this.fb.control<AlarmRuleCondition | null>(null), condition: this.fb.control<AlarmRuleCondition | null>(null, Validators.required),
alarmDetails: [null], alarmDetails: [null],
dashboardId: [null] dashboardId: [null]
}); });
@ -105,14 +114,12 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
} }
writeValue(value: AlarmRule): void { writeValue(value: AlarmRule): void {
if (value) { this.modelValue = value;
this.modelValue = value; const model = this.modelValue ? {
const model = this.modelValue ? { ...this.modelValue,
...this.modelValue, dashboardId: this.modelValue.dashboardId?.id
dashboardId: this.modelValue.dashboardId?.id } : null;
} : null; this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
}
} }
public openEditDetailsDialog($event: Event) { public openEditDetailsDialog($event: Event) {
@ -134,7 +141,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
}); });
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : {
alarmRule: { alarmRule: {
valid: false, valid: false,

6
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html

@ -34,7 +34,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" required class="flex-1"> <tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" [testScript]="testScript" required class="flex-1">
</tb-cf-alarm-rule> </tb-cf-alarm-rule>
</div> </div>
<button *ngIf="!disabled" <button *ngIf="!disabled"
@ -57,9 +57,7 @@
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4" [disabled]="createAlarmRulesFormArray().controls.length > 4"
type="button" type="button"
(click)="addCreateAlarmRule()" (click)="addCreateAlarmRule()">
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
{{ 'alarm-rule.add-create-alarm-rule' | translate }} {{ 'alarm-rule.add-create-alarm-rule' | translate }}
</button> </button>
</div> </div>

9
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts

@ -23,7 +23,7 @@ import {
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray, UntypedFormArray,
UntypedFormControl, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
@ -33,6 +33,7 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion"; import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({ @Component({
selector: 'tb-create-cf-alarm-rules', selector: 'tb-create-cf-alarm-rules',
@ -60,6 +61,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmSeverities = Object.keys(AlarmSeverity); alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity; alarmSeverityEnum = AlarmSeverity;
@ -156,8 +159,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
return null; return null;
} }
public validate(c: UntypedFormControl) { public validate(): ValidationErrors | null {
return (this.createAlarmRulesFormArray().length && this.createAlarmRulesFormGroup.valid) ? null : { return this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormArray().length > 0 ? null : {
createAlarmRules: { createAlarmRules: {
valid: false, valid: false,
}, },

5
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -67,7 +67,10 @@
<section class="tb-form-panel"> <section class="tb-form-panel">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.filter' | translate }}</div> <div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filter' | translate }}
</div>
<tb-toggle-select formControlName="operation" <tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs"> selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option> <tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -71,9 +71,7 @@
</section> </section>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
(click)="addFilter()" (click)="addFilter()"
type="button" type="button">
matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'alarm-rule.add-filter' | translate }} {{ 'alarm-rule.add-filter' | translate }}
</button> </button>

8
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -69,17 +69,13 @@
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(false)" (click)="addPredicate(false)"
type="button" type="button">
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(true)" (click)="addPredicate(true)"
type="button" type="button">
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-complex' | translate }} {{ 'filter.add-complex' | translate }}
</button> </button>
</div> </div>

5
ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html

@ -35,7 +35,10 @@
</div> </div>
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea> <textarea cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="apiKeyForm.get('description').hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="enabled"> <mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ 'api-key.enable' | translate }} {{ 'api-key.enable' | translate }}

4
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.html

@ -96,6 +96,10 @@
<ng-template #executeCommand> <ng-template #executeCommand>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div> <div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div>
<div class="tb-form-hint tb-primary-fill insecure-url-warning flex gap-2" [class.hidden]="secureUrl">
<mat-icon class="tb-mat-24">warning</mat-icon>
<span>{{ 'api-key.generated-api-key-insecure-url' | translate }}</span>
</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code" [data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown> <tb-markdown usePlainMarkdown containerClass="tb-command-code" [data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown>
</div> </div>
</ng-template> </ng-template>

17
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.scss

@ -15,9 +15,8 @@
*/ */
@import '../../src/scss/constants'; @import '../../src/scss/constants';
:host{ :host {
display: grid; display: grid;
width: 500px;
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100vh;
@ -26,6 +25,20 @@
.tb-install-instruction-text { .tb-install-instruction-text {
min-height: 42px; min-height: 42px;
} }
.insecure-url-warning {
.mat-icon {
color: #FAA405;
}
}
@media #{$mat-sm} {
width: 470px;
}
@media #{$mat-gt-sm} {
width: 720px;
}
} }
:host ::ng-deep { :host ::ng-deep {

5
ui-ngx/src/app/modules/home/components/api-key/api-key-generated-dialog.component.ts

@ -34,7 +34,10 @@ export interface ApiKeyGeneratedDialogData {
}) })
export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> { export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> {
apiKeyCommand = userInfoCommand(this.data.apiKey.value); private baseUrl = window.location.origin;
apiKeyCommand = userInfoCommand(this.baseUrl, this.data.apiKey.value);
secureUrl = this.baseUrl.startsWith('https');
selectedTab: number; selectedTab: number;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,

5
ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html

@ -21,7 +21,10 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea> <textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea>
<mat-hint align="end">{{input.value?.length || 0}}/255</mat-hint> <mat-error *ngIf="descriptionFormControl.hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
<mat-hint align="end">{{ input.value?.length || 0 }}/255</mat-hint>
</mat-form-field> </mat-form-field>
<div class="tb-edit-api-key-description-panel-buttons"> <div class="tb-edit-api-key-description-panel-buttons">
<button mat-button <button mat-button

4
ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts

@ -16,7 +16,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ApiKeyService } from '@core/http/api-key.service'; import { ApiKeyService } from '@core/http/api-key.service';
@ -37,7 +37,7 @@ export class EditApiKeyDescriptionPanelComponent implements OnInit {
@Output() @Output()
descriptionApplied = new EventEmitter<string>(); descriptionApplied = new EventEmitter<string>();
descriptionFormControl = this.fb.control<string>(null); descriptionFormControl = this.fb.control<string>(null, Validators.required);
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private popover: TbPopoverComponent<EditApiKeyDescriptionPanelComponent>, private popover: TbPopoverComponent<EditApiKeyDescriptionPanelComponent>,

21
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -24,7 +24,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs'; import { EMPTY, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -60,6 +60,7 @@ import { isObject } from '@core/utils';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
@ -256,6 +257,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.importExportService.openCalculatedFieldImportDialog() this.importExportService.openCalculatedFieldImportDialog()
.pipe( .pipe(
filter(Boolean), filter(Boolean),
switchMap(calculatedField => {
if (calculatedField.type === CalculatedFieldType.ALARM) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'),
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left',
duration: 5000
}));
return EMPTY;
}
return of(calculatedField);
}),
switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean), filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -295,9 +309,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> { private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if ( if (
calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
(calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true) (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true)
) { ) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
@ -313,7 +328,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: { data: {
arguments: resultArguments, arguments: resultArguments,
expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression, expression: expression ?? (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit openCalculatedFieldEdit

25
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html

@ -24,15 +24,8 @@
</div> </div>
} }
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
@if (!isOutputKey) { @if (!watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: { <ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
label: 'calculated-fields.argument-name',
required: 'calculated-fields.hint.argument-name-required',
duplicate: 'calculated-fields.hint.argument-name-duplicate',
pattern: 'calculated-fields.hint.argument-name-pattern',
maxlength: 'calculated-fields.hint.argument-name-max-length',
forbidden: 'calculated-fields.hint.argument-name-forbidden'
}"></ng-container>
} }
<ng-container> <ng-container>
@if (!hiddenEntityTypes) { @if (!hiddenEntityTypes) {
@ -105,7 +98,7 @@
<ng-container [ngTemplateOutlet]="timeseriesKeyAutocomplete"/> <ng-container [ngTemplateOutlet]="timeseriesKeyAutocomplete"/>
} }
<ng-template #timeseriesKeyAutocomplete> <ng-template #timeseriesKeyAutocomplete>
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/> <tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter" [enableAutocomplete]="!enableAutocomplete"/>
</ng-template> </ng-template>
</div> </div>
} @else { } @else {
@ -132,6 +125,7 @@
<tb-entity-key-autocomplete <tb-entity-key-autocomplete
formControlName="key" formControlName="key"
class="flex-1" class="flex-1"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute" [dataKeyType]="DataKeyType.attribute"
[entityFilter]="entityFilter" [entityFilter]="entityFilter"
[keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value" [keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value"
@ -140,15 +134,8 @@
} }
} }
</ng-container> </ng-container>
@if (isOutputKey) { @if (watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: { <ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
label: 'calculated-fields.output-key',
required: 'calculated-fields.hint.output-key-required',
duplicate: 'calculated-fields.hint.output-key-duplicate',
pattern: 'calculated-fields.hint.output-key-pattern',
maxlength: 'calculated-fields.hint.output-key-max-length',
forbidden: 'calculated-fields.hint.output-key-forbidden'
}"></ng-container>
} }
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
@if (!hiddenDefaultValue) { @if (!hiddenDefaultValue) {

34
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts

@ -71,7 +71,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() ownerId: EntityId; @Input() ownerId: EntityId;
@Input() isScript: boolean; @Input() isScript: boolean;
@Input() usedArgumentNames: string[]; @Input() usedArgumentNames: string[];
@Input() isOutputKey = false; @Input() watchKeyChange = false;
@Input() hiddenEntityTypes = false; @Input() hiddenEntityTypes = false;
@Input() hiddenEntityKeyTypes = false; @Input() hiddenEntityKeyTypes = false;
@Input() hiddenDefaultValue = false; @Input() hiddenDefaultValue = false;
@ -80,6 +80,14 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() predefinedEntityFilter: EntityFilter; @Input() predefinedEntityFilter: EntityFilter;
@Input() forbiddenNames = FORBIDDEN_NAMES; @Input() forbiddenNames = FORBIDDEN_NAMES;
@Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[];
@Input() argumentNameContext: {[key: string]: string} = {
label: 'calculated-fields.argument-name',
required: 'calculated-fields.hint.argument-name-required',
duplicate: 'calculated-fields.hint.argument-name-duplicate',
pattern: 'calculated-fields.hint.argument-name-pattern',
maxlength: 'calculated-fields.hint.argument-name-max-length',
forbidden: 'calculated-fields.hint.argument-name-forbidden'
};
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
@ -107,6 +115,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
entityFilter: EntityFilter; entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null); entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
readonly ArgumentType = ArgumentType; readonly ArgumentType = ArgumentType;
readonly DataKeyType = DataKeyType; readonly DataKeyType = DataKeyType;
@ -155,7 +165,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType(); this.setInitialEntityKeyType();
this.setInitialEntityType(); this.setInitialEntityType();
this.setWatchKeyChange(); if (this.watchKeyChange) {
this.setWatchKeyChange();
}
if (this.defaultValueRequired) { if (this.defaultValueRequired) {
this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); this.argumentFormGroup.get('defaultValue').addValidators(Validators.required);
@ -202,6 +214,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private updatedArgumentType(): void { private updatedArgumentType(): void {
let argumentType = ArgumentEntityType.Current; let argumentType = ArgumentEntityType.Current;
if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) {
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE);
argumentType = ArgumentEntityType.Owner; argumentType = ArgumentEntityType.Owner;
} else if (this.argument.refEntityId?.entityType) { } else if (this.argument.refEntityId?.entityType) {
argumentType = this.argument.refEntityId.entityType; argumentType = this.argument.refEntityId.entityType;
@ -270,6 +283,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
.pipe(distinctUntilChanged(), takeUntilDestroyed()) .pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => { .subscribe(type => {
this.argumentFormGroup.get('refEntityId').setValue(null); this.argumentFormGroup.get('refEntityId').setValue(null);
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner;
this.updatedRefEntityIdState(type); this.updatedRefEntityIdState(type);
if (!this.enableAttributeScopeSelection) { if (!this.enableAttributeScopeSelection) {
this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE);
@ -299,15 +313,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
private setWatchKeyChange(): void { private setWatchKeyChange(): void {
if (this.isOutputKey) { this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
this.refEntityKeyFormGroup.get('key').valueChanges.pipe( takeUntilDestroyed(this.destroyRef)
takeUntilDestroyed(this.destroyRef) ).subscribe((key) => {
).subscribe((key) => { if (this.argumentFormGroup.get('argumentName').pristine) {
if (this.argumentFormGroup.get('argumentName').pristine) { this.argumentFormGroup.get('argumentName').setValue(key);
this.argumentFormGroup.get('argumentName').setValue(key); }
} });
});
}
} }
private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void { private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void {

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html

@ -120,10 +120,10 @@
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row> <mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table> </table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" <div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center"> class="tb-prompt flex flex-1 items-end justify-center min-h-14">
{{ 'calculated-fields.no-arguments' | translate }} {{ 'calculated-fields.no-arguments' | translate }}
</div> </div>
@if (errorText) { @if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/> <tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
} }
</div> </div>
@ -134,7 +134,7 @@
color="primary" color="primary"
#button #button
(click)="manageArgument($event, button)" (click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF" [disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton"
> >
{{ 'calculated-fields.add-argument' | translate }} {{ 'calculated-fields.add-argument' | translate }}
</button> </button>

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts

@ -87,6 +87,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityName: string; @Input() entityName: string;
@Input() ownerId: EntityId; @Input() ownerId: EntityId;
@Input() isScript: boolean; @Input() isScript: boolean;
@Input() disabledAddButton = false;
@Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ -181,6 +183,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
ownerId: this.ownerId, ownerId: this.ownerId,
watchKeyChange: this.watchKeyChange,
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName), usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
}; };
this.popoverComponent = this.popoverService.displayPopover({ this.popoverComponent = this.popoverService.displayPopover({

10
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts

@ -93,7 +93,15 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa
this.displayColumns = ['name', 'type', 'key', 'actions']; this.displayColumns = ['name', 'type', 'key', 'actions'];
this.panelAdditionalCtx = { this.panelAdditionalCtx = {
argumentEntityTypes: [ArgumentEntityType.Current], argumentEntityTypes: [ArgumentEntityType.Current],
isOutputKey: true, argumentNameContext: {
label: 'calculated-fields.output-key',
required: 'calculated-fields.hint.output-key-required',
duplicate: 'calculated-fields.hint.output-key-duplicate',
pattern: 'calculated-fields.hint.output-key-pattern',
maxlength: 'calculated-fields.hint.output-key-max-length',
forbidden: 'calculated-fields.hint.output-key-forbidden'
},
watchKeyChange: true,
forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'], forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'],
}; };
} }

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -88,6 +88,7 @@
[entityId]="data.entityId" [entityId]="data.entityId"
[entityName]="data.entityName" [entityName]="data.entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component> ></tb-related-entities-aggregation-component>
} }
@case (CalculatedFieldType.ENTITY_AGGREGATION) { @case (CalculatedFieldType.ENTITY_AGGREGATION) {

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -105,19 +105,19 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
} }
onTestScript(): Observable<string> { onTestScript(expression?: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) { if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe( .pipe(
switchMap(event => { switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null; const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false); return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression);
}), }),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
} }
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
} }
private applyDialogData(): void { private applyDialogData(): void {

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -34,6 +34,7 @@
</div> </div>
<tb-calculated-field-metrics-table formControlName="metrics" <tb-calculated-field-metrics-table formControlName="metrics"
simpleMode simpleMode
[testScript]="testScript"
[arguments]="arguments$ | async" [arguments]="arguments$ | async"
></tb-calculated-field-metrics-table> ></tb-calculated-field-metrics-table>
</div> </div>

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -42,7 +42,7 @@ import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { merge } from 'rxjs'; import { merge, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import _moment from 'moment'; import _moment from 'moment';
@ -85,6 +85,8 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
@Input({required: true}) @Input({required: true})
entityName: string; entityName: string;
@Input() testScript: (expression?: string) => Observable<string>;
readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF;
readonly DayInSec = DAY / SECOND; readonly DayInSec = DAY / SECOND;

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

@ -176,7 +176,7 @@
</div> </div>
</ng-container> </ng-container>
<ng-container> <ng-container>
@if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current) { @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) {
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}"> <div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }} {{ 'calculated-fields.perimeter-attribute-key' | translate }}
@ -204,6 +204,7 @@
</mat-form-field> </mat-form-field>
} @else { } @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName" <tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter" [dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter"
[keyScopeType]="AttributeScope.SERVER_SCOPE"/> [keyScopeType]="AttributeScope.SERVER_SCOPE"/>
} }

13
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts

@ -97,6 +97,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
entityFilter: EntityFilter; entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null); entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentEntityType = ArgumentEntityType;
readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[];
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
@ -151,6 +153,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
} else { } else {
this.addKey(); this.addKey();
} }
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) &&
this.refEntityIdFormGroup.get('entityType').value === ArgumentEntityType.Owner;
this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones); this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones);
this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type); this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type);
@ -265,18 +269,17 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
) )
.pipe(debounceTime(50), takeUntilDestroyed()) .pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType)); .subscribe(() => this.updateEntityFilter(this.entityType));
this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset(''));
} }
private observeEntityTypeChanges(): void { private observeEntityTypeChanges(): void {
this.refEntityIdFormGroup.get('entityType').valueChanges this.refEntityIdFormGroup.get('entityType').valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed()) .pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => { .subscribe(type => {
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner;
this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); this.geofencingFormGroup.get('refEntityId').get('id').setValue(null);
const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery; this.geofencingFormGroup.get('perimeterKeyName').reset('');
this.geofencingFormGroup.get('refEntityId') const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type);
.get('id')[isEntityWithId ? 'enable' : 'disable'](); this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) { if (!isEntityWithId) {
this.entityNameSubject.next(null); this.entityNameSubject.next(null);
} }

46
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html

@ -27,7 +27,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate" [matTooltip]="'calculated-fields.metrics.metric-name-required' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -35,7 +35,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-duplicate' | translate" [matTooltip]="'calculated-fields.metrics.metric-name-duplicate' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -43,7 +43,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate" [matTooltip]="'calculated-fields.metrics.metric-name-pattern' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -51,7 +51,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate" [matTooltip]="'calculated-fields.metrics.metric-name-max-length' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -59,7 +59,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate" [matTooltip]="'calculated-fields.metrics.metric-name-forbidden' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -105,7 +105,24 @@
<div toolbarPrefixButton <div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div> </div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!arguments.length"
(click)="onTestScript('filter')">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func> </tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript('filter')"
[disabled]="!arguments.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
@ -146,7 +163,7 @@
} @else { } @else {
<tb-js-func required <tb-js-func required
formControlName="function" formControlName="function"
functionName="filter" functionName="map"
[functionArgs]="functionArgs" [functionArgs]="functionArgs"
[disableUndefinedCheck]="true" [disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL" [scriptLanguage]="ScriptLanguage.TBEL"
@ -157,7 +174,24 @@
<div toolbarPrefixButton <div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div> </div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!arguments.length"
(click)="onTestScript('input.function')">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func> </tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript('input.function')"
[disabled]="!arguments.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
} }
</ng-container> </ng-container>
@if (simpleMode) { @if (simpleMode) {

11
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts

@ -33,6 +33,7 @@ import { EntityFilter } from '@shared/models/query/query.models';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue {
allowFilter: boolean; allowFilter: boolean;
@ -52,6 +53,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
@Input() simpleMode: boolean; @Input() simpleMode: boolean;
@Input() editorCompleter: TbEditorCompleter; @Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules; @Input() highlightRules: AceHighlightRules;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
metricDataApplied = output<CalculatedFieldAggMetricValue>(); metricDataApplied = output<CalculatedFieldAggMetricValue>();
filterExpanded = false; filterExpanded = false;
@ -81,7 +83,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent> private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>,
) { ) {
this.observeFilterAllowChange(); this.observeFilterAllowChange();
this.observeInputTypeChange(); this.observeInputTypeChange();
@ -163,4 +165,11 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
this.metricForm.get('input.key').markAsTouched(); this.metricForm.get('input.key').markAsTouched();
} }
} }
onTestScript(scriptFunc: 'filter' | 'input.function') {
this.testScript(this.metricForm.get(scriptFunc).value).subscribe(expression => {
this.metricForm.get(scriptFunc).setValue(expression);
this.metricForm.get(scriptFunc).markAsDirty();
});
}
} }

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts

@ -56,6 +56,7 @@ import {
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; } from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component';
import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
@Component({ @Component({
selector: 'tb-calculated-field-metrics-table', selector: 'tb-calculated-field-metrics-table',
@ -80,6 +81,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
@Input() editorCompleter: TbEditorCompleter; @Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules; @Input() highlightRules: AceHighlightRules;
@Input({transform: booleanAttribute}) simpleMode: boolean = false; @Input({transform: booleanAttribute}) simpleMode: boolean = false;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ -166,6 +168,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
editorCompleter: this.editorCompleter, editorCompleter: this.editorCompleter,
highlightRules: this.highlightRules, highlightRules: this.highlightRules,
simpleMode: this.simpleMode, simpleMode: this.simpleMode,
testScript: this.testScript
}; };
this.popoverComponent = this.popoverService.displayPopover({ this.popoverComponent = this.popoverService.displayPopover({
trigger, trigger,

155
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -46,7 +46,7 @@
@if (hiddenName) { @if (hiddenName) {
<div class="grid items-start gap-3 has-[.simpleMode]:grid-cols-2 xs:grid-cols-1"> <div class="grid items-start gap-3 has-[.simpleMode]:grid-cols-2 xs:grid-cols-1">
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container> <ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
<ng-content select=".simpleMode"></ng-content> <ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
</div> </div>
} @else { } @else {
<div class="flex items-start gap-3 xs:flex-col xs:items-stretch"> <div class="flex items-start gap-3 xs:flex-col xs:items-stretch">
@ -74,93 +74,91 @@
</mat-form-field> </mat-form-field>
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container> <ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
</div> </div>
<ng-content select=".simpleMode"></ng-content> <ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
} }
} }
</div> </div>
<div class="tb-form-panel stroked" formGroupName="strategy"> <div class="tb-form-panel stroked" formGroupName="strategy">
<div class="flex flex-row items-center justify-between gap-2"> <mat-expansion-panel class="tb-settings" [disabled]="outputForm.get('strategy.type').value === OutputStrategyType.RULE_CHAIN">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}"> <mat-expansion-panel-header class="flex flex-row flex-wrap">
{{ 'calculated-fields.output-strategy.strategy' | translate }} <mat-panel-title>
</div> <div class="flex flex-1 flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill"> <div class="tb-form-panel-title" style="color: var(--mat-expansion-header-text-color)" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { {{ 'calculated-fields.output-strategy.strategy' | translate }}
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option> </div>
} <tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill" (click)="$event.stopPropagation()">
</tb-toggle-select> @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
</div> <tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { }
<div class="tb-form-panel stroked"> </tb-toggle-select>
<div class="tb-form-panel-title tb-normal" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.processing-options' | translate }}"> </div>
{{ 'calculated-fields.output-strategy.processing-options' | translate }} </mat-panel-title>
</div> </mat-expansion-panel-header>
<mat-chip-listbox multiple> @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>calculated-fields.output-strategy.processing-parameters</div>
@if (outputForm.get('type').value === OutputType.Timeseries) { @if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-chip-option <mat-slide-toggle class="mat-slide" formControlName="saveTimeSeries">
[selected]="outputForm.get('strategy.saveTimeSeries').value" <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-time-series' | translate }}">
[disabled]="outputForm.get('strategy.saveTimeSeries').disabled" <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-time-series</div>
(click)="toggleChip('saveTimeSeries')"> </div>
{{ 'calculated-fields.output-strategy.save-time-series' | translate }} </mat-slide-toggle>
</mat-chip-option> <mat-slide-toggle class="mat-slide" formControlName="saveLatest">
<mat-chip-option <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-latest-values' | translate }}">
[selected]="outputForm.get('strategy.saveLatest').value" <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-latest-values</div>
[disabled]="outputForm.get('strategy.saveLatest').disabled" </div>
(click)="toggleChip('saveLatest')"> </mat-slide-toggle>
{{ 'calculated-fields.output-strategy.save-latest-values' | translate }}
</mat-chip-option>
} @else { } @else {
<mat-chip-option <mat-slide-toggle class="mat-slide" formControlName="saveAttribute">
[selected]="outputForm.get('strategy.saveAttribute').value" <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-database' | translate }}">
[disabled]="outputForm.get('strategy.saveAttribute').disabled" <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-database</div>
(click)="toggleChip('saveAttribute')"> </div>
{{ 'calculated-fields.output-strategy.save-database' | translate }} </mat-slide-toggle>
</mat-chip-option>
} }
<mat-chip-option <mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate">
[selected]="outputForm.get('strategy.sendWsUpdate').value" <div tb-hint-tooltip-icon="{{ (outputForm.get('type').value === OutputType.Attribute
[disabled]="outputForm.get('strategy.sendWsUpdate').disabled" ? 'calculated-fields.output-strategy.hint.send-web-sockets-attribute'
(click)="toggleChip('sendWsUpdate')"> : 'calculated-fields.output-strategy.hint.send-web-sockets-time-series') | translate }}">
{{ 'calculated-fields.output-strategy.send-web-sockets' | translate }} <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div>
</mat-chip-option> </div>
<mat-chip-option </mat-slide-toggle>
[selected]="outputForm.get('strategy.processCfs').value" <mat-slide-toggle class="mat-slide" formControlName="processCfs">
[disabled]="outputForm.get('strategy.processCfs').disabled" <div tb-hint-tooltip-icon="{{ (outputForm.get('type').value === OutputType.Attribute
(click)="toggleChip('processCfs')"> ? 'calculated-fields.output-strategy.hint.save-calculated-fields-attribute'
{{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }} : 'calculated-fields.output-strategy.hint.save-calculated-fields-time-series') | translate }}">
</mat-chip-option> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div>
</mat-chip-listbox>
</div>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
} @else { @if (outputForm.get('type').value === OutputType.Attribute) {
<tb-time-unit-input <div class="tb-form-row flex-1">
required <mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
subscriptSizing="dynamic" <div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
appearance="outline" ? 'calculated-fields.output-strategy.hint.update-attribute-only-on-value-change-enabled'
sameWidthInputs : 'calculated-fields.output-strategy.hint.update-attribute-only-on-value-change') | translate }}">
labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}" <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attribute-only-on-value-change</div>
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}" </div>
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}" </mat-slide-toggle>
formControlName="ttl"> </div>
<mat-icon class="help-icon mr-2 cursor-pointer" } @else {
aria-hidden="false" <div class="tb-form-row space-between column-xs">
aria-label="help-icon" <mat-slide-toggle class="mat-slide" formControlName="useCustomTtl">
color="primary" <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
matSuffix <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.ttl</div>
matTooltip="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}"> </div>
help_outline </mat-slide-toggle>
</mat-icon> <tb-time-unit-input
</tb-time-unit-input> required
inlineField
class="medium-width"
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}"
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"
formControlName="ttl">
</tb-time-unit-input>
</div>
}
} }
} </mat-expansion-panel>
</div> </div>
</div> </div>
<ng-template #decimalsByDefaultField> <ng-template #decimalsByDefaultField>
@ -172,3 +170,6 @@
} }
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template #simpleContentTemplate>
<ng-content select=".simpleMode"></ng-content>
</ng-template>

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

Loading…
Cancel
Save