Browse Source

Merge remote-tracking branch 'upstream/master' into resource-upload

# Conflicts:
#	ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts
pull/14205/head
Vladyslav_Prykhodko 10 months ago
parent
commit
6f5d6597e6
  1. 2
      application/src/main/data/json/system/widget_types/timeseries_table.json
  2. 10
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 54
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  4. 12
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  5. 69
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  6. 3
      application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java
  7. 44
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  8. 22
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  9. 28
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  10. 9
      application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java
  11. 7
      application/src/main/java/org/thingsboard/server/controller/UsageInfoController.java
  12. 46
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  13. 42
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  14. 23
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  16. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java
  17. 45
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  18. 95
      application/src/main/java/org/thingsboard/server/service/security/auth/AbstractAuthenticationProvider.java
  19. 76
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  20. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  21. 12
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java
  22. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java
  23. 42
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  24. 2
      application/src/main/java/org/thingsboard/server/service/security/model/token/ApiKeyAuthRequest.java
  25. 101
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  26. 48
      application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java
  27. 16
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  28. 12
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  29. 2
      application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
  30. 14
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKey.java
  32. 20
      common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java
  33. 3
      dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java
  34. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java
  35. 3
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java
  36. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppBundleOauth2ClientEntity.java
  37. 1
      dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java
  38. 68
      ui-ngx/src/app/core/http/audit-log.service.ts
  39. 4
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  40. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  41. 25
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  42. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  43. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  44. 81
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  45. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  46. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  48. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  50. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  51. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  52. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  53. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  54. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  55. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  56. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  57. 61
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html
  58. 35
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss
  59. 198
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
  60. 20
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html
  61. 21
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss
  62. 38
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts
  63. 19
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  64. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  65. 25
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  66. 34
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  67. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  68. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  69. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  70. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  71. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  72. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  73. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  74. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  75. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  76. 46
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  77. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  78. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  79. 158
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  80. 27
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  81. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  82. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  83. 6
      ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts
  84. 4
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  85. 10
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  86. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html
  87. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  88. 35
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  89. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts
  90. 62
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  91. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  92. 8
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts
  93. 3
      ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts
  94. 4
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  95. 8
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts
  96. 4
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  97. 46
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  98. 2
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html
  99. 7
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  100. 2
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

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": [
{ {

10
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -84,6 +84,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@ -562,15 +563,20 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}; };
RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType());
List<EntityRelation> byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); List<EntityRelation> byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation)));
EntityId cfEntityId = cf.getEntityId();
Predicate<EntityId> matchesCfEntity = relatedEntity -> cfEntityId.equals(relatedEntity) || cfEntityId.equals(getProfileId(tenantId, relatedEntity));
if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) { if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) {
switch (relation.direction()) { switch (relation.direction()) {
case FROM -> { case FROM -> {
EntityRelation entityRelation = byRelationPathQuery.get(0); // only one supported EntityRelation entityRelation = byRelationPathQuery.get(0); // only one supported
result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getFrom())); EntityId relatedId = entityRelation.getFrom();
if (matchesCfEntity.test(relatedId)) {
result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), relatedId));
}
} }
case TO -> { case TO -> {
byRelationPathQuery.stream() byRelationPathQuery.stream()
.filter(entityRelation -> entityRelation.getTo().equals(cf.getEntityId())) .filter(entityRelation -> matchesCfEntity.test(entityRelation.getTo()))
.forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo()))); .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo())));
} }
} }

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/ctx/state/CalculatedFieldCtx.java

@ -651,8 +651,8 @@ public class CalculatedFieldCtx implements Closeable {
} }
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
boolean metricsChanged = thisConfig.getMetrics().equals(otherConfig.getMetrics()); boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics());
boolean watermarkChanged = thisConfig.getWatermark().equals(otherConfig.getWatermark()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark());
return metricsChanged || watermarkChanged; return metricsChanged || watermarkChanged;
} }
return false; return false;

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java

@ -17,12 +17,13 @@ package org.thingsboard.server.service.cf.ctx.state.aggregation.function;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction;
import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
public class CountUniqueAggEntry implements AggEntry { public class CountUniqueAggEntry implements AggEntry {
private Set<String> items; private final Set<String> items = new HashSet<>();
@Override @Override
public void update(Object value) { public void update(Object value) {

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

@ -45,7 +45,9 @@ import java.util.Comparator;
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.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultMetricArgumentEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultMetricArgumentEntry;
@ -190,10 +192,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
args.forEach((argName, argEntryIntervalStatus) -> { args.forEach((argName, argEntryIntervalStatus) -> {
if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) { if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
processMetric(intervalEntry, argName, false, results); processArgument(intervalEntry, argName, false, results);
} else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
processMetric(intervalEntry, argName, true, results); processArgument(intervalEntry, argName, true, results);
} }
}); });
} }
@ -206,38 +208,39 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
if (argEntryIntervalStatus.argsUpdated()) { if (argEntryIntervalStatus.argsUpdated()) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
argEntryIntervalStatus.setLastArgsRefreshTs(-1); argEntryIntervalStatus.setLastArgsRefreshTs(-1);
processMetric(intervalEntry, argName, false, results); processArgument(intervalEntry, argName, false, results);
} else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
processMetric(intervalEntry, argName, true, results); processArgument(intervalEntry, argName, true, results);
} }
} }
}); });
} }
private void processMetric(AggIntervalEntry intervalEntry, private void processArgument(AggIntervalEntry intervalEntry,
String argName, String argName,
boolean useDefault, boolean useDefault,
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) { Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) {
String metricName = findMetricName(argName); Set<String> metrics = findMetrics(argName);
if (metricName != null) { if (!metrics.isEmpty()) {
AggMetric metric = metrics.get(metricName); metrics.forEach(metricName -> {
String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey(); AggMetric metric = this.metrics.get(metricName);
ArgumentEntry metricEntry = useDefault String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey();
? createDefaultMetricArgumentEntry(argKey, metric) ArgumentEntry metricEntry = useDefault
: cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry); ? createDefaultMetricArgumentEntry(argKey, metric)
if (!metricEntry.isEmpty()) { : cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry);
results.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(metricName, metricEntry); if (!metricEntry.isEmpty()) {
} results.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(metricName, metricEntry);
}
});
} }
} }
private String findMetricName(String argName) { private Set<String> findMetrics(String argName) {
return metrics.entrySet().stream() return metrics.entrySet().stream()
.filter(e -> ((AggKeyInput) e.getValue().getInput()).getKey().equals(argName)) .filter(e -> ((AggKeyInput) e.getValue().getInput()).getKey().equals(argName))
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.findFirst() .collect(Collectors.toSet());
.orElse(null);
} }
protected ArrayNode toResult(Map<AggIntervalEntry, Map<String, ArgumentEntry>> results, Integer precision) { protected ArrayNode toResult(Map<AggIntervalEntry, Map<String, ArgumentEntry>> results, Integer precision) {

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) {}

101
application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java

@ -99,16 +99,17 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L);
long intervalEndTs = customInterval.getCurrentIntervalEndTs(); long intervalEndTs = customInterval.getCurrentIntervalEndTs();
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null); CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null);
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
await().alias("create CF and no telemetry during interval -> save metric with default value") await().alias("create CF and no telemetry during interval -> save metric with default value")
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("9999"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("9999");
assertThat(result.get("avgConsumption").get(0).get("value").isNull()).isTrue();
}); });
} }
@ -130,15 +131,16 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null); CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null);
await().alias("create CF -> perform aggregation after interval end") await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133");
}); });
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", tsInInterval_1)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", tsInInterval_1));
@ -147,9 +149,10 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133");
}); });
} }
@ -172,15 +175,16 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
Watermark watermark = new Watermark(10); Watermark watermark = new Watermark(10);
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, watermark); CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, watermark);
await().alias("create CF -> perform aggregation after interval end") await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS) .atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133");
}); });
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1));
@ -189,13 +193,14 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
.atMost(2 * 10, TimeUnit.SECONDS) .atMost(2 * 10, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("600"); assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("600");
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("200");
}); });
} }
private CalculatedField createTotalConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) { private CalculatedField createConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) {
Map<String, Argument> arguments = new HashMap<>(); Map<String, Argument> arguments = new HashMap<>();
Argument argument = new Argument(); Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null));
@ -209,10 +214,86 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
consumption.setDefaultValue(9999L); consumption.setDefaultValue(9999L);
aggMetrics.put("consumption", consumption); aggMetrics.put("consumption", consumption);
AggMetric avgEnergyConsumption = new AggMetric();
avgEnergyConsumption.setFunction(AggFunction.AVG);
avgEnergyConsumption.setInput(new AggKeyInput("en"));
aggMetrics.put("avgConsumption", avgEnergyConsumption);
TimeSeriesOutput output = new TimeSeriesOutput();
output.setDecimalsByDefault(0);
return createAggCf("Consumption", entityId,
aggInterval,
watermark,
arguments,
aggMetrics,
output);
}
@Test
public void testCreateCfWith2Arguments_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs();
long tsBeforeInterval = currentIntervalStartTs - 1000;
long tsInInterval_1 = currentIntervalStartTs + 1000;
long tsInInterval_2 = currentIntervalStartTs + 500;
long tsInInterval_3 = currentIntervalStartTs + 200;
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":43}}", tsBeforeInterval));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":39}}", tsInInterval_1));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":27}}", tsInInterval_2));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":50}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis();
CalculatedField consumptionCF = createCFWith2Args(device.getId(), customInterval, null);
await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgTemperature");
assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
assertThat(result.get("avgTemperature").get(0).get("value").asText()).isEqualTo("39");
});
}
private CalculatedField createCFWith2Args(EntityId entityId, AggInterval aggInterval, Watermark watermark) {
Map<String, Argument> arguments = new HashMap<>();
Argument energy = new Argument();
energy.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null));
arguments.put("en", energy);
Argument temperature = new Argument();
temperature.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
arguments.put("temp", temperature);
Map<String, AggMetric> aggMetrics = new HashMap<>();
AggMetric consumption = new AggMetric();
consumption.setFunction(AggFunction.SUM);
consumption.setInput(new AggKeyInput("en"));
consumption.setDefaultValue(9999L);
aggMetrics.put("consumption", consumption);
AggMetric avgTemperature = new AggMetric();
avgTemperature.setFunction(AggFunction.AVG);
avgTemperature.setInput(new AggKeyInput("temp"));
aggMetrics.put("avgTemperature", avgTemperature);
TimeSeriesOutput output = new TimeSeriesOutput(); TimeSeriesOutput output = new TimeSeriesOutput();
output.setDecimalsByDefault(0); output.setDecimalsByDefault(0);
return createAggCf("Consumption per minute", entityId, return createAggCf("CF with 2 args", entityId,
aggInterval, aggInterval,
watermark, watermark,
arguments, arguments,

48
application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java

@ -673,6 +673,54 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
}); });
} }
@Test
public void testTheSameRelationTypeAndKeyButDifferentCfs_checkAggregation() throws Exception {
postTelemetry(device1.getId(), "{\"temperature\":24.2}");
postTelemetry(device2.getId(), "{\"temperature\":19.6}");
CalculatedField cf = createAvgTemperatureCF(asset.getId());
Asset asset2 = createAsset("Asset 2", assetProfile.getId());
Device device3 = createDevice("Device 3", "1234567890333");
createEntityRelation(asset2.getId(), device3.getId(), "Contains");
postTelemetry(device3.getId(), "{\"temperature\":10.2}");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setName("Average temperature");
calculatedField.setEntityId(asset2.getId());
calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION);
var config = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration();
AggMetric avgMetric = new AggMetric();
avgMetric.setFunction(AggFunction.AVG);
avgMetric.setInput(new AggKeyInput("temp"));
config.setMetrics(Map.of("avgTemperature_2", avgMetric));
calculatedField.setConfiguration(config);
calculatedField.setDebugSettings(DebugSettings.all());
saveCalculatedField(calculatedField);
await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode avgTemperature = getLatestTelemetry(asset.getId(), "avgTemperature", "avgTemperature_2");
assertThat(avgTemperature).isNotNull();
assertThat(avgTemperature.get("avgTemperature").get(0).get("value").asText()).isEqualTo("24");
assertThat(avgTemperature.get("avgTemperature_2").get(0).get("value").isNull()).isTrue();
});
postTelemetry(device1.getId(), "{\"temperature\":26.2}");
await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode avgTemperature = getLatestTelemetry(asset.getId(), "avgTemperature", "avgTemperature_2");
assertThat(avgTemperature).isNotNull();
assertThat(avgTemperature.get("avgTemperature").get(0).get("value").asText()).isEqualTo("26");
assertThat(avgTemperature.get("avgTemperature_2").get(0).get("value").isNull()).isTrue();
});
}
private void checkInitialCalculation() { private void checkInitialCalculation() {
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)

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

@ -38,8 +38,6 @@ import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
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.boot.test.mock.mockito.SpyBean;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -53,6 +51,8 @@ import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockPart; import org.springframework.mock.web.MockPart;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
@ -167,6 +167,7 @@ import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileServ
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.system.SystemPatchApplier;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -295,18 +296,21 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired @Autowired
private JwtTokenFactory jwtTokenFactory; private JwtTokenFactory jwtTokenFactory;
@SpyBean
protected MailService mailService;
@Autowired @Autowired
protected InMemoryStorage storage; protected InMemoryStorage storage;
@Autowired @Autowired
protected JdbcTemplate jdbcTemplate; protected JdbcTemplate jdbcTemplate;
@MockBean @MockitoSpyBean
protected MailService mailService;
@MockitoBean
protected CfRocksDb cfRocksDb; protected CfRocksDb cfRocksDb;
@MockitoBean
protected SystemPatchApplier systemPatchApplier;
@Rule @Rule
public TestRule watcher = new TestWatcher() { public TestRule watcher = new TestWatcher() {
protected void starting(Description description) { protected void starting(Description description) {

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++) {

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

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

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;

68
ui-ngx/src/app/core/http/audit-log.service.ts

@ -15,12 +15,12 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { AuditLog } from '@shared/models/audit-log.models'; import { AuditLog, AuditLogFilter } from '@shared/models/audit-log.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
@Injectable({ @Injectable({
@ -32,28 +32,58 @@ export class AuditLogService {
private http: HttpClient private http: HttpClient
) { } ) { }
public getAuditLogs(pageLink: TimePageLink, public getAuditLogs(pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
config?: RequestConfig): Observable<PageData<AuditLog>> { public getAuditLogs(pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
return this.http.get<PageData<AuditLog>>(`/api/audit/logs${pageLink.toQuery()}`, public getAuditLogs(
defaultHttpOptionsFromConfig(config)); pageLink: TimePageLink,
filtersOrConfig?: AuditLogFilter | RequestConfig,
config?: RequestConfig
): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(
`/api/audit/logs${pageLink.toQuery()}`,
createDefaultHttpOptions(filtersOrConfig, config)
);
} }
public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
config?: RequestConfig): Observable<PageData<AuditLog>> { public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, public getAuditLogsByCustomerId(
defaultHttpOptionsFromConfig(config)); customerId: string,
pageLink: TimePageLink,
filtersOrConfig?: AuditLogFilter | RequestConfig,
config?: RequestConfig
): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(
`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`,
createDefaultHttpOptions(filtersOrConfig, config)
);
} }
public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
config?: RequestConfig): Observable<PageData<AuditLog>> { public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`, public getAuditLogsByUserId(
defaultHttpOptionsFromConfig(config)); userId: string,
pageLink: TimePageLink,
filtersOrConfig?: AuditLogFilter | RequestConfig,
config?: RequestConfig
): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(
`/api/audit/logs/user/${userId}${pageLink.toQuery()}`,
createDefaultHttpOptions(filtersOrConfig, config)
);
} }
public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
config?: RequestConfig): Observable<PageData<AuditLog>> { public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, public getAuditLogsByEntityId(
defaultHttpOptionsFromConfig(config)); entityId: EntityId,
pageLink: TimePageLink,
filtersOrConfig?: AuditLogFilter | RequestConfig,
config?: RequestConfig
): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(
`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`,
createDefaultHttpOptions(filtersOrConfig, config)
);
} }
} }

4
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -125,6 +125,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
this.showError(req.method + ': ' + req.url + '<br/>' + this.showError(req.method + ': ' + req.url + '<br/>' +
errorResponse.status + ': ' + errorResponse.statusText); errorResponse.status + ': ' + errorResponse.statusText);
} }
} else if (errorResponse.status === 504) {
if (!ignoreErrors) {
this.showError('Request timeout');
}
} else { } else {
unhandled = true; unhandled = true;
} }

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>

25
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({
@ -182,4 +190,19 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
} }
}); });
} }
onTestScript(expression: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
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";

81
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> {
@ -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
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { import {
@ -38,11 +39,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({
@ -117,7 +120,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
}, },
@ -223,4 +226,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>

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

@ -21,8 +21,7 @@ import {
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl, UntypedFormControl,
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,10 +125,8 @@ 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() {
@ -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) {

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 {

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

@ -34,6 +34,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,6 +66,13 @@ 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({

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>

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

@ -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;
@ -157,7 +160,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
} }
public validate(c: UntypedFormControl) { public validate(c: UntypedFormControl) {
return (this.createAlarmRulesFormArray().length && this.createAlarmRulesFormGroup.valid) ? null : { return this.createAlarmRulesFormArray().length ? 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>

61
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html

@ -0,0 +1,61 @@
<!--
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.
-->
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleAuditLogFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
<ng-template #auditLogFilterPanel>
<form class="mat-content mat-padding flex flex-col" [formGroup]="auditLogFilterForm" (ngSubmit)="update()">
<div class="tb-form-panel flex-1 no-padding no-border">
<div class="tb-form-row flex-1 space-between column-xs">
<div class="fixed-title-width" translate>audit-log.filter-types</div>
<tb-string-items-list appearance="outline"
class="flex flex-1 gt-xs:w-64"
subscriptSizing="dynamic"
[placeholder]="'audit-log.filter-type-add' | translate"
[predefinedValues]="predefinedTypeValues"
formControlName="actionTypes">
</tb-string-items-list>
</div>
</div>
<div class="tb-panel-actions flex flex-row items-center justify-end">
<button type="button"
mat-button
color="primary"
(click)="reset()">
{{ 'action.reset' | translate }}
</button>
<span class="flex-1"></span>
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="auditLogFilterForm.invalid || !auditLogFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>

35
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss

@ -0,0 +1,35 @@
/**
* 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.
*/
:host {
display: flex;
max-width: 100%;
padding-right: 20px;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

198
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts

@ -0,0 +1,198 @@
///
/// 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.
///
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { isDefined, isDefinedAndNotNull } from '@app/core/utils';
import { StringItemsOption } from '@app/shared/components/string-items-list.component';
import {
ActionType,
actionTypeTranslations,
auditLogFilterEquals,
AuditLogFilter,
POSITION_MAP,
} from '@app/shared/public-api';
import { TranslateService } from '@ngx-translate/core';
// @dynamic
@Component({
selector: 'tb-audit-log-filter',
templateUrl: './audit-log-filter.component.html',
styleUrls: ['./audit-log-filter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AuditLogFilterComponent),
multi: true
}
]
})
export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
@ViewChild('auditLogFilterPanel')
auditLogFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
ActionType = ActionType;
actionTypes = Object.values(ActionType);
actionTypeTranslations = actionTypeTranslations;
buttonDisplayValue = this.translate.instant('audit-log.filter');
auditLogFilterForm: UntypedFormGroup;
auditLogOverlayRef: OverlayRef;
initialAuditLogFilter: AuditLogFilter;
private auditLogFilter: AuditLogFilter;
private propagateChange = (_: any) => {};
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private destroyRef: DestroyRef) {}
ngOnInit(): void {
this.auditLogFilterForm = this.fb.group({
actionTypes: [[]]
});
this.auditLogFilterForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
this.updateValidators();
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.auditLogFilterForm.disable({emitEvent: false});
} else {
this.auditLogFilterForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(auditLogFilter?: AuditLogFilter): void {
this.auditLogFilter = auditLogFilter;
if(!this.initialAuditLogFilter && isDefined(this.auditLogFilter)) {
this.initialAuditLogFilter = this.auditLogFilterForm.getRawValue();
}
this.updateButtonDisplayValue();
this.updateAuditLogFilterForm(auditLogFilter);
}
get predefinedTypeValues (): StringItemsOption[] {
return [...this.actionTypes].map(type => ({
name: this.translate.instant(this.actionTypeTranslations.get(type)),
value: type
}));
}
private updateValidators() {
}
toggleAuditLogFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(this.nativeElement)
.withPositions([POSITION_MAP.bottomLeft]);
this.auditLogOverlayRef = this.overlay.create(config);
this.auditLogOverlayRef.backdropClick().subscribe(() => {
this.auditLogOverlayRef.dispose();
});
this.auditLogOverlayRef.attach(new TemplatePortal(this.auditLogFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateAuditLogFilterForm(this.auditLogFilter);
this.auditLogOverlayRef.dispose();
}
update() {
this.auditLogFilterUpdated(this.auditLogFilterForm.value);
this.auditLogOverlayRef.dispose();
}
reset() {
if (this.initialAuditLogFilter) {
if (!auditLogFilterEquals(this.auditLogFilter, this.initialAuditLogFilter)) {
this.updateAuditLogFilterForm(this.initialAuditLogFilter);
this.auditLogFilterForm.markAsDirty();
}
}
}
private updateAuditLogFilterForm(auditLogFilter?: AuditLogFilter) {
this.auditLogFilterForm.patchValue({
actionTypes: auditLogFilter?.actionTypes,
}, {emitEvent: false});
this.updateValidators();
}
private auditLogFilterUpdated(auditLogFilter: AuditLogFilter) {
this.auditLogFilter = auditLogFilter;
this.updateButtonDisplayValue();
this.propagateChange(this.auditLogFilter);
}
private updateButtonDisplayValue() {
const filterTextParts: string[] = [];
if (isDefinedAndNotNull(this.auditLogFilter?.actionTypes)) {
const actionTypes = this.auditLogFilter.actionTypes;
actionTypes.forEach(actionType => filterTextParts.push(this.translate.instant(this.actionTypeTranslations.get(actionType as ActionType))))
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('audit-log.filter-title') + `: ${filterTextParts.join(', ')}`;
}
this.cd.detectChanges();
}
}

20
ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html

@ -0,0 +1,20 @@
<!--
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.
-->
<tb-audit-log-filter [ngModel]="entitiesTableConfig.componentsData.auditLogFilter"
(ngModelChange)="auditLogFiltersChanged($event)"
></tb-audit-log-filter>

21
ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss

@ -0,0 +1,21 @@
/**
* 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.
*/
:host {
padding-right: 8px;
overflow: hidden;
max-width: 100%;
}

38
ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts

@ -0,0 +1,38 @@
///
/// 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.
///
import { Component } from '@angular/core';
import { AppState } from '@app/core/public-api';
import { AuditLog, AuditLogFilter, TimePageLink } from '@app/shared/public-api';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Store } from '@ngrx/store';
@Component({
selector: 'tb-audit-log-header',
templateUrl: './audit-log-header.component.html',
styles: ``
})
export class AuditLogHeaderComponent extends EntityTableHeaderComponent<AuditLog, TimePageLink> {
constructor(protected store: Store<AppState>) {
super(store)
}
auditLogFiltersChanged(auditLogFilter: AuditLogFilter) {
this.entitiesTableConfig.componentsData.auditLogFilter = auditLogFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true, true);
}
}

19
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -25,7 +25,8 @@ import {
ActionType, ActionType,
actionTypeTranslations, actionTypeTranslations,
AuditLog, AuditLog,
AuditLogMode AuditLogMode,
AuditLogFilter
} from '@shared/models/audit-log.models'; } from '@shared/models/audit-log.models';
import { import {
AliasEntityType, AliasEntityType,
@ -48,6 +49,8 @@ import {
AuditLogDetailsDialogComponent, AuditLogDetailsDialogComponent,
AuditLogDetailsDialogData AuditLogDetailsDialogData
} from '@home/components/audit-log/audit-log-details-dialog.component'; } from '@home/components/audit-log/audit-log-details-dialog.component';
import { deepClone } from '@app/core/utils';
import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component';
export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLink> { export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLink> {
@ -71,6 +74,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.searchEnabled = true; this.searchEnabled = true;
this.addEnabled = false; this.addEnabled = false;
this.entitiesDeleteEnabled = false; this.entitiesDeleteEnabled = false;
this.headerComponent = AuditLogHeaderComponent;
this.actionsColumnTitle = 'audit-log.details'; this.actionsColumnTitle = 'audit-log.details';
this.entityTranslations = { this.entityTranslations = {
noEntities: 'audit-log.no-audit-logs-prompt', noEntities: 'audit-log.no-audit-logs-prompt',
@ -79,6 +83,10 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.entityResources = { this.entityResources = {
} as EntityTypeResource<AuditLog>; } as EntityTypeResource<AuditLog>;
this.componentsData = {
auditLogFilter: []
};
this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink); this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink);
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
@ -139,15 +147,16 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
} }
fetchAuditLogs(pageLink: TimePageLink): Observable<PageData<AuditLog>> { fetchAuditLogs(pageLink: TimePageLink): Observable<PageData<AuditLog>> {
const auditLogFilter: AuditLogFilter = deepClone(this.componentsData?.auditLogFilter) || {};
switch (this.auditLogMode) { switch (this.auditLogMode) {
case AuditLogMode.TENANT: case AuditLogMode.TENANT:
return this.auditLogService.getAuditLogs(pageLink); return this.auditLogService.getAuditLogs(pageLink, auditLogFilter);
case AuditLogMode.ENTITY: case AuditLogMode.ENTITY:
return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink); return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink, auditLogFilter);
case AuditLogMode.USER: case AuditLogMode.USER:
return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink); return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink, auditLogFilter);
case AuditLogMode.CUSTOMER: case AuditLogMode.CUSTOMER:
return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink); return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink, auditLogFilter);
} }
} }

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,

158
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,94 @@
</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()">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
}
</tb-toggle-select>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
@if (outputForm.get('type').value === OutputType.Timeseries) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveTimeSeries">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-time-series' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-time-series</div>
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveLatest">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-latest-values' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-latest-values</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveAttribute">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-database' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-database</div>
</div>
</mat-slide-toggle>
</div>
} }
</tb-toggle-select> <div class="tb-form-row flex-1">
</div> <mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate">
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.send-web-sockets' | translate }}">
<div class="tb-form-panel stroked"> <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div>
<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-slide-toggle>
</div> </div>
<mat-chip-listbox multiple>
@if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-chip-option
[selected]="outputForm.get('strategy.saveTimeSeries').value"
[disabled]="outputForm.get('strategy.saveTimeSeries').disabled"
(click)="toggleChip('saveTimeSeries')">
{{ 'calculated-fields.output-strategy.save-time-series' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.saveLatest').value"
[disabled]="outputForm.get('strategy.saveLatest').disabled"
(click)="toggleChip('saveLatest')">
{{ 'calculated-fields.output-strategy.save-latest-values' | translate }}
</mat-chip-option>
} @else {
<mat-chip-option
[selected]="outputForm.get('strategy.saveAttribute').value"
[disabled]="outputForm.get('strategy.saveAttribute').disabled"
(click)="toggleChip('saveAttribute')">
{{ 'calculated-fields.output-strategy.save-database' | translate }}
</mat-chip-option>
}
<mat-chip-option
[selected]="outputForm.get('strategy.sendWsUpdate').value"
[disabled]="outputForm.get('strategy.sendWsUpdate').disabled"
(click)="toggleChip('sendWsUpdate')">
{{ 'calculated-fields.output-strategy.send-web-sockets' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.processCfs').value"
[disabled]="outputForm.get('strategy.processCfs').disabled"
(click)="toggleChip('processCfs')">
{{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1"> <div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange"> <mat-slide-toggle class="mat-slide" formControlName="processCfs">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value <div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-calculated-fields' | translate }}">
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled' <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div>
: '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-attributes-only-on-value-change-enabled'
sameWidthInputs : 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}" <div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-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 +173,6 @@
} }
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template #simpleContentTemplate>
<ng-content select=".simpleMode"></ng-content>
</ng-template>

27
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -38,6 +38,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { merge } from 'rxjs';
import { deepClone } from '@core/utils';
@Component({ @Component({
selector: 'tb-calculate-field-output', selector: 'tb-calculate-field-output',
@ -102,6 +104,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
sendWsUpdate: [true], sendWsUpdate: [true],
processCfs: [true], processCfs: [true],
updateAttributesOnlyOnValueChange: [true], updateAttributesOnlyOnValueChange: [true],
useCustomTtl: [false],
ttl: [0] ttl: [0]
}) })
}); });
@ -116,7 +119,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
this.updatedStrategy(); this.updatedStrategy();
}); });
this.outputForm.get('strategy.type').valueChanges merge(
this.outputForm.get('strategy.type').valueChanges,
this.outputForm.get('strategy.useCustomTtl').valueChanges
)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.updatedStrategy(); this.updatedStrategy();
@ -151,6 +157,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void {
this.outputForm.patchValue(value, {emitEvent: false}); this.outputForm.patchValue(value, {emitEvent: false});
if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) {
this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false});
}
this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); this.outputForm.get('type').updateValueAndValidity({onlySelf: true});
} }
@ -171,13 +180,6 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
} }
} }
toggleChip(controlName: string) {
const control = this.outputForm.get('strategy').get(controlName);
if (control && control.enabled) {
control.setValue(!control.value);
}
}
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) {
if (this.simpleMode && 'name' in value) { if (this.simpleMode && 'name' in value) {
value.name = value.name?.trim() ?? ''; value.name = value.name?.trim() ?? '';
@ -185,6 +187,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (this.disableType) { if (this.disableType) {
value.type = this.outputForm.get('type').value; value.type = this.outputForm.get('type').value;
} }
if (value.type === OutputType.Timeseries && value.strategy.type === OutputStrategyType.IMMEDIATE) {
value = deepClone(value);
delete (value.strategy as any).useCustomTtl
}
this.propagateChange(value); this.propagateChange(value);
} }
@ -227,7 +233,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
} else { } else {
this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false}); this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false});
this.outputForm.get('strategy.saveLatest').enable({emitEvent: false}); this.outputForm.get('strategy.saveLatest').enable({emitEvent: false});
this.outputForm.get('strategy.ttl').enable({emitEvent: false}); this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false});
if (this.outputForm.get('strategy.useCustomTtl').value) {
this.outputForm.get('strategy.ttl').enable({emitEvent: false});
}
} }
} }
} }

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

@ -55,6 +55,7 @@
{{ 'calculated-fields.metrics.metrics' | translate }} {{ 'calculated-fields.metrics.metrics' | translate }}
</div> </div>
<tb-calculated-field-metrics-table formControlName="metrics" <tb-calculated-field-metrics-table formControlName="metrics"
[testScript]="testScript"
[arguments]="arguments$ | async" [arguments]="arguments$ | async"
[highlightRules]="argumentsHighlightRules$ | async" [highlightRules]="argumentsHighlightRules$ | async"
[editorCompleter]="argumentsEditorCompleter$ | async" [editorCompleter]="argumentsEditorCompleter$ | async"

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

@ -73,6 +73,9 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
@Input({required: true}) @Input({required: true})
entityName: string; entityName: string;
@Input({required: true})
testScript: (expression?: string) => Observable<string>;
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType; readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType; readonly OutputType = OutputType;

6
ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts

@ -17,8 +17,6 @@
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Directive, Input, OnInit } from '@angular/core'; import { Directive, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -41,8 +39,8 @@ export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>,
return this.entitiesTableConfigValue; return this.entitiesTableConfigValue;
} }
protected constructor(protected store: Store<AppState>) { protected constructor(...args: unknown[]) {
super(store); super();
} }
ngOnInit() { ngOnInit() {

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

@ -23,6 +23,7 @@ import { DetailsPanelComponent } from '@home/components/details-panel.component'
import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit-log-details-dialog.component'; import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit-log-details-dialog.component';
import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EventTableComponent } from '@home/components/event/event-table.component'; import { EventTableComponent } from '@home/components/event/event-table.component';
import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component'; import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component';
@ -202,6 +203,7 @@ import { AddApiKeyDialogComponent } from '@home/components/api-key/add-api-key-d
import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/edit-api-key-description-panel.component'; import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/edit-api-key-description-panel.component';
import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component';
import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component';
import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component";
@NgModule({ @NgModule({
declarations: declarations:
@ -358,6 +360,8 @@ import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-t
AddApiKeyDialogComponent, AddApiKeyDialogComponent,
EditApiKeyDescriptionPanelComponent, EditApiKeyDescriptionPanelComponent,
ApiKeyGeneratedDialogComponent, ApiKeyGeneratedDialogComponent,
AuditLogHeaderComponent,
AuditLogFilterComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

10
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -91,11 +91,11 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
onDataUpdated: (subscription) => { onDataUpdated: (subscription) => {
const data = formattedDataFormDatasourceData(subscription.data); const data = formattedDataFormDatasourceData(subscription.data);
this.apiUsages.forEach(key => { this.apiUsages.forEach(key => {
const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; const progress = (this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0) ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0;
key.progress = isFinite(progress) ? progress : 0; key.progress = isFinite(progress) ? progress : 0;
key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
}); });
this.cd.detectChanges(); this.cd.detectChanges();
} }
@ -114,6 +114,10 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
} }
private isFiniteNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
updateState($event: MouseEvent, stateName: string) { updateState($event: MouseEvent, stateName: string) {
$event?.preventDefault(); $event?.preventDefault();
if (stateName?.length) { if (stateName?.length) {

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html

@ -20,6 +20,7 @@
<div class="tb-form-panel-title" translate>widget-config.datasource</div> <div class="tb-form-panel-title" translate>widget-config.datasource</div>
<tb-entity-alias-select <tb-entity-alias-select
class="tb-entity-alias-field" class="tb-entity-alias-field"
showLabel
tbRequired tbRequired
[appearance]="'outline'" [appearance]="'outline'"
[aliasController]="context.aliasController" [aliasController]="context.aliasController"

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -115,15 +115,22 @@
<mat-slide-toggle class="mat-slide" formControlName="useEntityLabel"> <mat-slide-toggle class="mat-slide" formControlName="useEntityLabel">
{{ 'widgets.table.use-entity-label-tab-name' | translate }} {{ 'widgets.table.use-entity-label-tab-name' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div class="tb-form-row justify-between"> <div class="tb-form-row justify-between column-xs" formGroupName="sortOrder">
<div class="fixed-title-width" translate>widgets.table.sort-by</div> <div class="fixed-title-width" translate>widgets.table.sort-by</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> <div class="flex flex-1 flex-row justify-end gap-2 ">
<mat-select formControlName="tabSortKey" placeholder="{{ 'widget-config.set' | translate }}"> <mat-form-field class="flex-lt-lg medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-option [value]="TabSortKey.TIMESTAMP">{{ 'widgets.table.sort-timestamp-option' | translate }}</mat-option> <mat-select formControlName="property" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="TabSortKey.NAME_ASC">{{ 'widgets.table.sort-asc' | translate }}</mat-option> <mat-option [value]="entityFields.createdTime.value">{{ entityFields.createdTime.name | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_DESC">{{ 'widgets.table.sort-desc' | translate }}</mat-option> <mat-option [value]="entityFields.name.value">{{ entityFields.name.name | translate }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-lt-lg medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="Direction.ASC">{{ 'common.sort-asc' | translate }}</mat-option>
<mat-option [value]="Direction.DESC">{{ 'common.sort-desc' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div> </div>
</div> </div>
<div class="tb-form-panel tb-slide-toggle"> <div class="tb-form-panel tb-slide-toggle">

35
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts

@ -20,7 +20,8 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models';
import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-table-widget.component' import { Direction } from '@shared/models/page/sort-order';
import { entityFields } from '@shared/models/entity.models';
@Component({ @Component({
selector: 'tb-timeseries-table-widget-settings', selector: 'tb-timeseries-table-widget-settings',
@ -29,7 +30,8 @@ import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-t
}) })
export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent {
TabSortKey = TabSortKey; entityFields = entityFields;
Direction = Direction;
timeseriesTableWidgetSettingsForm: UntypedFormGroup; timeseriesTableWidgetSettingsForm: UntypedFormGroup;
pageStepSizeValues = []; pageStepSizeValues = [];
@ -62,13 +64,19 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: false, disableStickyHeader: false,
useRowStyleFunction: false, useRowStyleFunction: false,
rowStyleFunction: '', rowStyleFunction: '',
tabSortKey: TabSortKey.TIMESTAMP sortOrder: {
property: this.entityFields.createdTime.keyName,
direction: Direction.DESC
}
}; };
} }
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize; settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize;
settings.tabSortKey = settings.tabSortKey ?? TabSortKey.TIMESTAMP; settings.sortOrder = {
property: settings.sortOrder?.property || this.entityFields.createdTime.keyName,
direction: settings.sortOrder?.direction || Direction.DESC
};
this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement);
return settings; return settings;
} }
@ -99,7 +107,16 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: [settings.disableStickyHeader, []], disableStickyHeader: [settings.disableStickyHeader, []],
useRowStyleFunction: [settings.useRowStyleFunction, []], useRowStyleFunction: [settings.useRowStyleFunction, []],
rowStyleFunction: [settings.rowStyleFunction, [Validators.required]], rowStyleFunction: [settings.rowStyleFunction, [Validators.required]],
tabSortKey: [settings.tabSortKey, []], sortOrder: this.fb.group({
property: [
settings.sortOrder.property,
Validators.required
],
direction: [
settings.sortOrder.direction,
Validators.required
]
})
}); });
} }
@ -107,6 +124,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement']; return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement'];
} }
protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings {
settings.sortOrder = {
property: settings.sortOrder?.property || this.entityFields.createdTime.keyName,
direction: settings.sortOrder?.direction || Direction.DESC
};
return settings;
}
protected updateValidators(emitEvent: boolean, trigger: string) { protected updateValidators(emitEvent: boolean, trigger: string) {
if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') { if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') {
this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').reset(); this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').reset();

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts

@ -64,6 +64,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
callbacks: EntityAliasSelectCallbacks; callbacks: EntityAliasSelectCallbacks;
@Input() @Input()
@coerceBoolean()
showLabel: boolean; showLabel: boolean;
@ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete;

62
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -106,19 +106,14 @@ import { ComponentPortal } from '@angular/cdk/portal';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models'; import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models';
import { entityFields } from '@shared/models/entity.models';
export enum TabSortKey {
NAME_ASC = 'NAME_ASC',
NAME_DESC = 'NAME_DESC',
TIMESTAMP = 'timestamp'
}
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
showMilliseconds: boolean; showMilliseconds: boolean;
hideEmptyLines: boolean; hideEmptyLines: boolean;
dateFormat: DateFormatSettings; dateFormat: DateFormatSettings;
tabSortKey: TabSortKey; sortOrder: SortOrder;
} }
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings { interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
@ -400,33 +395,39 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.updateDatasources(); this.updateDatasources();
} }
private getTabLabel(source: Datasource, entityLabelCache:Map<string,string>):string { private getTabLabel(source: Datasource):string {
if (entityLabelCache.has(source.entityId)) { const value = this.useEntityLabel
return entityLabelCache.get(source.entityId);
}
const value = this.useEntityLabel
? (source.entityLabel || source.entityName) ? (source.entityLabel || source.entityName)
: source.entityName; : source.entityName;
const translated = this.utils.customTranslation(value); return this.utils.customTranslation(value);
entityLabelCache.set(source.entityId, translated);
return translated;
} }
private sortDatasources(source: Datasource[], entityLabelCache:Map<string,string>): Datasource[] { private sortDatasources(source: TimeseriesTableSource[]) {
source.forEach(ds => this.getTabLabel(ds, entityLabelCache)); const property = this.settings?.sortOrder?.property;
const direction = this.settings?.sortOrder?.direction;
const isAsc = direction === Direction.ASC;
if (property === entityFields.name.keyName) {
const collator = new Intl.Collator(undefined, {
sensitivity: "variant",
numeric: true,
ignorePunctuation: false
});
source.sort((a, b) => {
const valueA = a.displayName || '';
const valueB = b.displayName || '';
if (this.settings.tabSortKey === TabSortKey.TIMESTAMP) { return isAsc
return source; ? collator.compare(valueA, valueB)
: collator.compare(valueB, valueA);
});
} else if (property === entityFields.createdTime.keyName) {
if (isAsc) {
source.reverse();
}
} }
return source.sort((a, b) => {
const valueA = entityLabelCache.get(a.entityId);
const valueB = entityLabelCache.get(b.entityId);
return this.settings.tabSortKey === TabSortKey.NAME_ASC
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
});
} }
private updateDatasources() { private updateDatasources() {
@ -434,11 +435,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sourceIndex = 0; this.sourceIndex = 0;
let keyOffset = 0; let keyOffset = 0;
let latestKeyOffset = 0; let latestKeyOffset = 0;
const entityLabelCache = new Map<string,string>();
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
if (this.datasources) { if (this.datasources) {
const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache); for (const datasource of this.datasources) {
for (const datasource of sortedDatasources) {
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
const source = {} as TimeseriesTableSource; const source = {} as TimeseriesTableSource;
source.header = this.prepareHeader(datasource); source.header = this.prepareHeader(datasource);
@ -455,7 +454,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
source.pageLink = new PageLink(pageSize, 0, null, sortOrder); source.pageLink = new PageLink(pageSize, 0, null, sortOrder);
source.rowDataTemplate = {}; source.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null; source.rowDataTemplate.Timestamp = null;
source.displayName = entityLabelCache.get(datasource.entityId); source.displayName = this.getTabLabel(datasource);
if (this.showTimestamp) { if (this.showTimestamp) {
source.displayedColumns.push('0'); source.displayedColumns.push('0');
} }
@ -475,6 +474,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
} }
if (this.sources.length) { if (this.sources.length) {
this.sortDatasources(this.sources);
this.sources.forEach((source, index) => { this.sources.forEach((source, index) => {
this.prepareDisplayedColumn(index); this.prepareDisplayedColumn(index);
source.displayedColumns = this.displayedColumns[index].filter(value => value.display).map(value => value.def); source.displayedColumns = this.displayedColumns[index].filter(value => value.display).map(value => value.def);

4
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -33,11 +33,11 @@
</mat-tab> </mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) { @if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab"> <mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="entity.tenantId"/> <tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="ownerId"/>
</mat-tab> </mat-tab>
<mat-tab #alarmRules="matTab" <mat-tab #alarmRules="matTab"
label="{{'alarm-rule.alarm-rules' | translate }}"> label="{{'alarm-rule.alarm-rules' | translate }}">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table> <tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="ownerId"></tb-alarm-rules-table>
</mat-tab> </mat-tab>
} }
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab"> <mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">

8
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts

@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { AssetInfo } from '@app/shared/models/asset.models'; import { AssetInfo } from '@app/shared/models/asset.models';
import { EntityId } from "@shared/models/id/entity-id";
@Component({ @Component({
selector: 'tb-asset-tabs', selector: 'tb-asset-tabs',
@ -27,6 +28,8 @@ import { AssetInfo } from '@app/shared/models/asset.models';
}) })
export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> { export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>) {
super(store); super(store);
} }
@ -35,4 +38,9 @@ export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
super.ngOnInit(); super.ngOnInit();
} }
protected setEntity(entity: AssetInfo) {
this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId;
super.setEntity(entity);
}
} }

3
ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts

@ -20,8 +20,7 @@ import { SharedModule } from '@shared/shared.module';
import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module'; import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module';
@NgModule({ @NgModule({
declarations: [ declarations: [],
],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,

4
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html

@ -33,10 +33,10 @@
</mat-tab> </mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) { @if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab"> <mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="entity.tenantId"/> <tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="ownerId"/>
</mat-tab> </mat-tab>
<mat-tab #calculatedFieldsAlarmRules="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}"> <mat-tab #calculatedFieldsAlarmRules="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}">
<tb-alarm-rules-table [active]="calculatedFieldsAlarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table> <tb-alarm-rules-table [active]="calculatedFieldsAlarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="ownerId"></tb-alarm-rules-table>
</mat-tab> </mat-tab>
} }
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab"> <mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">

8
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts

@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { DeviceInfo } from '@shared/models/device.models'; import { DeviceInfo } from '@shared/models/device.models';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { EntityId } from "@shared/models/id/entity-id";
@Component({ @Component({
selector: 'tb-device-tabs', selector: 'tb-device-tabs',
@ -27,6 +28,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone
}) })
export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> { export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>) {
super(store); super(store);
} }
@ -35,4 +38,9 @@ export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
super.ngOnInit(); super.ngOnInit();
} }
protected setEntity(entity: DeviceInfo) {
this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId;
super.setEntity(entity);
}
} }

4
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -233,7 +233,9 @@
<div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor"> <div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div> <div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }} {{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button> @if (preview.processedTemplates.MICROSOFT_TEAMS.button?.enabled) {
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button>
}
</div> </div>
</section> </section>
</div> </div>

46
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html

@ -44,8 +44,8 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> <div class="grid gt-sm:grid-cols-3 gt-sm:gap-4">
<mat-form-field class="max-w-30% flex-full xs:max-w-full"> <mat-form-field class="mat-block">
<mat-label translate>notification.action-type</mat-label> <mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType"> <mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" <mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes"
@ -54,32 +54,28 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-1" @if(actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK) {
*ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK; else dashboardSelector"> <mat-form-field class="gt-sm:col-span-2">
<mat-label translate>notification.link</mat-label> <mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required> <input matInput formControlName="link" required>
<mat-error <mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('required')"> *ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }} {{ 'notification.link-required' | translate }}
</mat-error> </mat-error>
<mat-error <mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')"> *ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate : {{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength} {length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}} }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<ng-template #dashboardSelector> } @else {
<tb-dashboard-autocomplete <tb-dashboard-autocomplete required formControlName="dashboardId">
class="max-w-35% flex-full xs:max-w-full"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete> </tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete class="max-w-35% flex-full xs:max-w-full" <tb-dashboard-state-autocomplete [dashboardId]="actionButtonConfigForm.get('dashboardId').value"
[dashboardId]="actionButtonConfigForm.get('dashboardId').value"
formControlName="dashboardState"> formControlName="dashboardState">
</tb-dashboard-state-autocomplete> </tb-dashboard-state-autocomplete>
</ng-template> }
</div> </div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle" <mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.DASHBOARD"> *ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.DASHBOARD">

2
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html

@ -43,7 +43,7 @@
@for (key of filteredKeys$ | async; track key) { @for (key of filteredKeys$ | async; track key) {
<mat-option [value]="key"><span [innerHTML]="key | highlight: searchText"></span></mat-option> <mat-option [value]="key"><span [innerHTML]="key | highlight: searchText"></span></mat-option>
} @empty { } @empty {
@if (!this.keyControl.value) { @if (!this.keyControl.value && enableAutocomplete) {
<mat-option [value]="''">{{ 'entity.no-keys-found' | translate }}</mat-option> <mat-option [value]="''">{{ 'entity.no-keys-found' | translate }}</mat-option>
} }
} }

7
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts

@ -66,6 +66,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
@Input() placeholder = this.translate.instant('action.set'); @Input() placeholder = this.translate.instant('action.set');
@Input() requiredText = this.translate.instant('common.hint.key-required'); @Input() requiredText = this.translate.instant('common.hint.key-required');
@Input() enableAutocomplete = true;
entityFilter = input.required<EntityFilter>(); entityFilter = input.required<EntityFilter>();
dataKeyType = input.required<DataKeyType>(); dataKeyType = input.required<DataKeyType>();
@ -81,12 +82,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
keys$ = this.keyInputSubject.asObservable() keys$ = this.keyInputSubject.asObservable()
.pipe( .pipe(
switchMap(() => { switchMap(() => {
if (!this.enableAutocomplete) {
return of([] as string[]);
}
return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({ return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({
pageLink: { page: 0, pageSize: 100 }, pageLink: { page: 0, pageSize: 100 },
entityFilter: this.entityFilter(), entityFilter: this.entityFilter(),
}, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true}); }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true});
}), }),
map(result => { map(result => {
if (Array.isArray(result)) {
return result;
}
this.cachedResult = result; this.cachedResult = result;
switch (this.dataKeyType()) { switch (this.dataKeyType()) {
case DataKeyType.attribute: case DataKeyType.attribute:

2
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

@ -204,7 +204,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
case EntityType.CALCULATED_FIELD: case EntityType.CALCULATED_FIELD:
this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-type') this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-type')
: this.translate.instant('alarm-rule.any-type'); : this.translate.instant('alarm-rule.any-type');
this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-rule'); this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-type');
this.noSubtypesMathingText = 'alarm-rule.no-alarm-rule-types-matching'; this.noSubtypesMathingText = 'alarm-rule.no-alarm-rule-types-matching';
this.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty'; this.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty';
break; break;

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

Loading…
Cancel
Save