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

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.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@ -562,15 +563,20 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
};
RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType());
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()) {
switch (relation.direction()) {
case FROM -> {
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 -> {
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())));
}
}

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.RequestHeader;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
@ -120,7 +118,7 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/serverTime")
@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();
}
@ -132,7 +130,7 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/maxDatapointsLimit")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000")))
public long getMaxDatapointsLimit() throws ThingsboardException {
public long getMaxDatapointsLimit() {
return maxDatapointsLimit;
}
@ -154,11 +152,11 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/{dashboardId}")
public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception {
@PathVariable(DASHBOARD_ID) String strDashboardId,
@Parameter(description = INCLUDE_RESOURCES_DESCRIPTION)
@RequestParam(value = INCLUDE_RESOURCES, required = false) boolean includeResources,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception {
checkParameter(DASHBOARD_ID, strDashboardId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
@ -179,9 +177,9 @@ public class DashboardController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/dashboard")
public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
@RequestBody Dashboard dashboard,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception {
@RequestBody Dashboard dashboard,
@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
HttpServletResponse response) throws Exception {
dashboard.setTenantId(getTenantId());
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
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. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/customer/public/dashboard/{dashboardId}")
public Dashboard assignDashboardToPublicCustomer(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@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. " +
"Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/customer/public/dashboard/{dashboardId}")
public Dashboard unassignDashboardFromPublicCustomer(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@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 +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"})
public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@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. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/dashboards", params = {"pageSize", "page"})
public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@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. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"})
public PageData<DashboardInfo> getCustomerDashboards(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@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. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/dashboard/home/info")
public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
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. " +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/dashboard/home/info")
public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
Tenant tenant = tenantService.findTenantById(getTenantId());
JsonNode additionalInfo = tenant.getAdditionalInfo();
@ -491,7 +482,7 @@ public class DashboardController extends BaseController {
notes = "Update the home dashboard assignment for the current tenant. " +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST)
@PostMapping(value = "/tenant/dashboard/home/info")
@ResponseStatus(value = HttpStatus.OK)
public void setTenantHomeDashboardInfo(
@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." +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}")
public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
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." +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}")
public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
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. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"})
public PageData<DashboardInfo> getEdgeDashboards(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@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.BulkImportResult;
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.dao.exception.DataValidationException;
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.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@ -268,7 +266,7 @@ public class EdgeController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
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));
} else {
return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink));
@ -295,7 +293,7 @@ public class EdgeController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
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));
} else {
return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink));
@ -359,7 +357,7 @@ public class EdgeController extends BaseController {
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageData<Edge> result;
if (type != null && type.trim().length() > 0) {
if (type != null && !type.trim().isEmpty()) {
result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink);
} else {
result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
@ -394,7 +392,7 @@ public class EdgeController extends BaseController {
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageData<EdgeInfo> result;
if (type != null && type.trim().length() > 0) {
if (type != null && !type.trim().isEmpty()) {
result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink);
} else {
result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
@ -470,7 +468,7 @@ public class EdgeController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/edge/sync/{edgeId}")
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);
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
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 org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
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.EdgeController.EDGE_ID;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@ -102,8 +100,7 @@ public class EntityViewController extends BaseController {
notes = "Fetch the EntityView object based on the provided entity view id. "
+ ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/entityView/{entityViewId}")
public EntityView getEntityViewById(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@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. "
+ ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/entityView/info/{entityViewId}")
public EntityViewInfo getEntityViewInfoById(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@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." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/entityView")
public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView,
@ -156,7 +151,7 @@ public class EntityViewController extends BaseController {
notes = "Delete the EntityView object based on the provided entity view id. "
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/entityView/{entityViewId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteEntityView(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@ -170,8 +165,7 @@ public class EntityViewController extends BaseController {
@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)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/entityViews", params = {"entityViewName"})
public EntityView getTenantEntityView(
@Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException {
@ -182,8 +176,7 @@ public class EntityViewController extends BaseController {
@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)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/customer/{customerId}/entityView/{entityViewId}")
public EntityView assignEntityViewToCustomer(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -204,8 +197,7 @@ public class EntityViewController extends BaseController {
@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)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/customer/entityView/{entityViewId}")
public EntityView unassignEntityViewFromCustomer(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@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. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"})
public PageData<EntityView> getCustomerEntityViews(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -247,7 +238,7 @@ public class EntityViewController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
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));
} else {
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 +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"})
public PageData<EntityViewInfo> getCustomerEntityViewInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -280,7 +270,7 @@ public class EntityViewController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
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));
} else {
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 +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/entityViews", params = {"pageSize", "page"})
public PageData<EntityView> getTenantEntityViews(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -309,7 +298,7 @@ public class EntityViewController extends BaseController {
TenantId tenantId = getCurrentUser().getTenantId();
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));
} else {
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 +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"})
public PageData<EntityViewInfo> getTenantEntityViewInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -337,7 +325,7 @@ public class EntityViewController extends BaseController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
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));
} else {
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. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityViews", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/entityViews")
public List<EntityView> findByQuery(
@Parameter(description = "The entity view search query JSON")
@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."
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/types", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/entityView/types")
public List<EntitySubtype> getEntityViewTypes() throws ThingsboardException, ExecutionException, InterruptedException {
SecurityUser user = getCurrentUser();
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. " +
"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')")
@RequestMapping(value = "/customer/public/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/customer/public/entityView/{entityViewId}")
public EntityView assignEntityViewToPublicCustomer(
@Parameter(description = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
@ -406,8 +391,7 @@ public class EntityViewController extends BaseController {
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.")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/edge/{edgeId}/entityView/{entityViewId}")
public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -430,8 +414,7 @@ public class EntityViewController extends BaseController {
EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once 'unassign' command will be delivered to edge service, it's going to remove entity view locally.")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/edge/{edgeId}/entityView/{entityViewId}")
public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -448,8 +431,7 @@ public class EntityViewController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"})
public PageData<EntityView> getEdgeEntityViews(
@PathVariable(EDGE_ID) String strEdgeId,
@RequestParam int pageSize,
@ -466,7 +448,7 @@ public class EntityViewController extends BaseController {
checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
PageData<EntityView> nonFilteredResult;
if (type != null && type.trim().length() > 0) {
if (type != null && !type.trim().isEmpty()) {
nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink);
} else {
nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
@ -485,4 +467,5 @@ public class EntityViewController extends BaseController {
nonFilteredResult.hasNext());
return checkNotNull(filteredResult);
}
}

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

@ -15,9 +15,6 @@
*/
package org.thingsboard.server.controller;
/**
* Created by ashvayka on 17.05.18.
*/
public class TbUrlConstants {
public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry";
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.security.access.prepost.PreAuthorize;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
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.TS_STRICT_DATA_EXAMPLE;
/**
* Created by ashvayka on 22.03.18.
*/
@RestController
@TbCoreComponent
@RequestMapping(TbUrlConstants.TELEMETRY_URL_PREFIX)
@ -172,8 +168,7 @@ public class TelemetryController extends BaseController {
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes")
public DeferredResult<ResponseEntity> getAttributeKeys(
@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 {
@ -187,8 +182,7 @@ public class TelemetryController extends BaseController {
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributeKeysByScope(
@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,
@ -205,8 +199,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/values/attributes")
public DeferredResult<ResponseEntity> getAttributes(
@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,
@ -229,8 +222,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributesByScope(
@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,
@ -247,8 +239,7 @@ public class TelemetryController extends BaseController {
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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/keys/timeseries")
public DeferredResult<ResponseEntity> getTimeseriesKeys(
@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 {
@ -269,8 +260,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries")
public DeferredResult<ResponseEntity> getLatestTimeseries(
@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,
@ -294,8 +284,7 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
@ResponseBody
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"})
public DeferredResult<ResponseEntity> getTimeseries(
@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,
@ -418,8 +407,7 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}")
public DeferredResult<ResponseEntity> saveEntityTelemetry(
@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,
@ -442,8 +430,7 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}")
public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(
@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,
@ -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."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/{entityType}/{entityId}/timeseries/delete")
public DeferredResult<ResponseEntity> deleteEntityTimeseries(
@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,
@ -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."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/{deviceId}/{scope}")
public DeferredResult<ResponseEntity> deleteDeviceAttributes(
@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,
@ -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."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/{entityType}/{entityId}/{scope}")
public DeferredResult<ResponseEntity> deleteEntityAttributes(
@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,

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

@ -21,12 +21,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Tenant;
@ -70,8 +71,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenant (getTenantById)",
notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/{tenantId}")
public Tenant getTenantById(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@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. " +
TENANT_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/info/{tenantId}")
public TenantInfo getTenantInfoById(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@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." +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/tenant")
public Tenant saveTenant(@Parameter(description = "A JSON value representing the tenant.")
@RequestBody Tenant tenant) throws Exception {
checkEntity(tenant.getId(), tenant, Resource.TENANT);
@ -116,7 +114,7 @@ public class TenantController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/tenant/{tenantId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION)
@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)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenants", params = {"pageSize", "page"})
public PageData<Tenant> getTenants(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@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. "
+ TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantInfos", params = {"pageSize", "page"})
public PageData<TenantInfo> getTenantInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@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 org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityInfo;
@ -74,8 +74,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profile (getTenantProfileById)",
notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantProfile/{tenantProfileId}")
public TenantProfile getTenantProfileById(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -87,8 +86,7 @@ public class TenantProfileController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantProfileInfo/{tenantProfileId}")
public EntityInfo getTenantProfileInfoById(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -100,8 +98,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)",
notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantProfileInfo/default")
public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException {
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." +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/tenantProfile")
public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.")
@Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
TenantProfile oldProfile;
@ -198,7 +194,7 @@ public class TenantProfileController extends BaseController {
@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)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/tenantProfile/{tenantProfileId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteTenantProfile(@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
@ -211,8 +207,7 @@ public class TenantProfileController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/tenantProfile/{tenantProfileId}/default")
public TenantProfile setDefaultTenantProfile(
@Parameter(description = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@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)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantProfiles", params = {"pageSize", "page"})
public PageData<TenantProfile> getTenantProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@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. "
+ TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"})
public PageData<EntityInfo> getTenantProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -269,5 +262,4 @@ public class TenantProfileController extends BaseController {
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.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.config.annotations.ApiOperation;
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. " +
"The actual value of the base url is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/uiSettings/helpBaseUrl", method = RequestMethod.GET)
@ResponseBody
public String getHelpBaseUrl() throws ThingsboardException {
@GetMapping(value = "/uiSettings/helpBaseUrl")
public String getHelpBaseUrl() {
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 org.springframework.beans.factory.annotation.Autowired;
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.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -37,9 +36,9 @@ public class UsageInfoController extends BaseController {
private UsageInfoService usageInfoService;
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/usage", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/usage")
public UsageInfo getTenantUsageInfo() throws ThingsboardException {
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.RequestHeader;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
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 'CUSTOMER_USER', the server checks that the requested user is owned by the same customer.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/user/{userId}")
public User getUserById(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@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 'TENANT_ADMIN', it is possible to login as any customer user. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/user/tokenAccessEnabled")
public boolean isUserTokenAccessEnabled() {
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 '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')")
@RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/user/{userId}/token")
public JwtPair getUserToken(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@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." +
"\n\nAvailable for users with 'SYS_ADMIN', 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/user")
public User saveUser(
@Parameter(description = "A JSON value representing the User.", required = true)
@RequestBody User user,
@ -206,7 +200,7 @@ public class UserController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/sendActivationMail", method = RequestMethod.POST)
@PostMapping(value = "/user/sendActivationMail")
@ResponseStatus(value = HttpStatus.OK)
public void sendActivationEmail(
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain")
@ResponseBody
public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
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). " +
"Referencing non-existing User Id will cause an error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/user/{userId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteUser(
@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." +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/users", params = {"pageSize", "page"})
public PageData<User> getUsers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@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 " +
"lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users/info", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/users/info")
public PageData<UserEmailInfo> findUsersByQuery(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -339,8 +330,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Tenant Users (getTenantAdmins)",
notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"})
public PageData<User> getTenantAdmins(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId,
@ -363,8 +353,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Customer Users (getCustomerUsers)",
notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"})
public PageData<User> getCustomerUsers(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -389,8 +378,7 @@ public class UserController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/user/{userId}/userCredentialsEnabled")
public void setUserCredentialsEnabled(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId,
@ -398,7 +386,7 @@ public class UserController extends BaseController {
@RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
UserId userId = new UserId(toUUID(strUserId));
User user = checkUserId(userId, Operation.WRITE);
checkUserId(userId, Operation.WRITE);
TenantId tenantId = getCurrentUser().getTenantId();
userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
@ -412,8 +400,7 @@ public class UserController extends BaseController {
"Search is been executed by email, firstName and lastName fields. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"})
public PageData<UserEmailInfo> getUsersForAssign(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@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 " +
"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')")
@RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/user/settings/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException {
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 " +
"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')")
@RequestMapping(value = "/user/settings/{type}/{paths}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/user/settings/{type}/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths,
@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)",
notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/user/dashboards/{dashboardId}/{action}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/user/dashboards/{dashboardId}/{action}")
public UserDashboardsInfo reportUserDashboardAction(
@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@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 org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
@ -111,8 +111,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeInfo/{widgetTypeId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypeInfo/{widgetTypeId}")
public WidgetTypeInfo getWidgetTypeInfoById(
@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@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." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/widgetType")
public WidgetTypeDetails saveWidgetType(
@Parameter(description = "A JSON value representing the Widget Type Details.", required = true)
@RequestBody WidgetTypeDetails widgetTypeDetails,
@ -153,7 +151,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/widgetType/{widgetTypeId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteWidgetType(
@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 + " " +
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypes", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypes", params = {"pageSize", "page"})
public PageData<WidgetTypeInfo> getWidgetTypes(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -215,8 +212,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"})
@Deprecated
public List<WidgetType> getBundleWidgetTypesByBundleAlias(
@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)",
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')")
@RequestMapping(value = "/widgetTypes", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"})
public List<WidgetType> getBundleWidgetTypes(
@Parameter(description = "Widget Bundle Id", required = true)
@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)",
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')")
@RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"})
@Deprecated
public List<WidgetTypeDetails> getBundleWidgetTypesDetailsByBundleAlias(
@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)",
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')")
@RequestMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypesDetails", params = {"widgetsBundleId"})
public List<WidgetTypeDetails> getBundleWidgetTypesDetails(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ -291,8 +284,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"})
public List<String> getBundleWidgetTypeFqns(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@ -303,8 +295,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"})
@Deprecated
public List<WidgetTypeInfo> getBundleWidgetTypesInfosByBundleAlias(
@Parameter(description = "System or Tenant", required = true)
@ -325,8 +316,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"})
public PageData<WidgetTypeInfo> getBundleWidgetTypesInfos(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ -357,8 +347,7 @@ public class WidgetTypeController extends AutoCommitController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"})
@Deprecated
public WidgetType getWidgetTypeByBundleAliasAndTypeAlias(
@Parameter(description = "System or Tenant", required = true)
@ -382,8 +371,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get Widget Type (getWidgetType)",
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')")
@RequestMapping(value = "/widgetType", params = {"fqn"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetType", params = {"fqn"})
public WidgetType getWidgetType(
@Parameter(description = "Widget Type fqn", required = true)
@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 org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -79,8 +80,7 @@ public class WidgetsBundleController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetsBundle/{widgetsBundleId}")
public WidgetsBundle getWidgetsBundleById(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@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." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/widgetsBundle")
public WidgetsBundle saveWidgetsBundle(
@Parameter(description = "A JSON value representing the Widget Bundle.", required = true)
@RequestBody WidgetsBundle widgetsBundle) throws Exception {
@ -126,7 +125,7 @@ public class WidgetsBundleController extends BaseController {
@ApiOperation(value = "Update widgets bundle widgets types list (updateWidgetsBundleWidgetTypes)",
notes = "Updates widgets bundle widgets list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes", method = RequestMethod.POST)
@PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes")
@ResponseStatus(value = HttpStatus.OK)
public void updateWidgetsBundleWidgetTypes(
@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)",
notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns", method = RequestMethod.POST)
@PostMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns")
@ResponseStatus(value = HttpStatus.OK)
public void updateWidgetsBundleWidgetFqns(
@Parameter(description = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@ -169,7 +168,7 @@ public class WidgetsBundleController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.DELETE)
@DeleteMapping(value = "/widgetsBundle/{widgetsBundleId}")
@ResponseStatus(value = HttpStatus.OK)
public void deleteWidgetsBundle(
@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 + " " +
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetsBundles", params = {"pageSize", "page"})
public PageData<WidgetsBundle> getWidgetsBundles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -223,8 +221,7 @@ public class WidgetsBundleController extends BaseController {
@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)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/widgetsBundles")
public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
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
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
boolean metricsChanged = thisConfig.getMetrics().equals(otherConfig.getMetrics());
boolean watermarkChanged = thisConfig.getWatermark().equals(otherConfig.getWatermark());
boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics());
boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark());
return metricsChanged || watermarkChanged;
}
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 java.util.HashSet;
import java.util.Optional;
import java.util.Set;
public class CountUniqueAggEntry implements AggEntry {
private Set<String> items;
private final Set<String> items = new HashSet<>();
@Override
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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultMetricArgumentEntry;
@ -190,10 +192,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
args.forEach((argName, argEntryIntervalStatus) -> {
if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
processMetric(intervalEntry, argName, false, results);
processArgument(intervalEntry, argName, false, results);
} else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {
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()) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
argEntryIntervalStatus.setLastArgsRefreshTs(-1);
processMetric(intervalEntry, argName, false, results);
processArgument(intervalEntry, argName, false, results);
} else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {
argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis());
processMetric(intervalEntry, argName, true, results);
processArgument(intervalEntry, argName, true, results);
}
}
});
}
private void processMetric(AggIntervalEntry intervalEntry,
String argName,
boolean useDefault,
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) {
String metricName = findMetricName(argName);
if (metricName != null) {
AggMetric metric = metrics.get(metricName);
String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey();
ArgumentEntry metricEntry = useDefault
? createDefaultMetricArgumentEntry(argKey, metric)
: cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry);
if (!metricEntry.isEmpty()) {
results.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(metricName, metricEntry);
}
private void processArgument(AggIntervalEntry intervalEntry,
String argName,
boolean useDefault,
Map<AggIntervalEntry, Map<String, ArgumentEntry>> results) {
Set<String> metrics = findMetrics(argName);
if (!metrics.isEmpty()) {
metrics.forEach(metricName -> {
AggMetric metric = this.metrics.get(metricName);
String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey();
ArgumentEntry metricEntry = useDefault
? createDefaultMetricArgumentEntry(argKey, metric)
: cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry);
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()
.filter(e -> ((AggKeyInput) e.getValue().getInput()).getKey().equals(argName))
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
.collect(Collectors.toSet());
}
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;
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.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
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.UserId;
import org.thingsboard.server.common.data.security.Authority;
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.TokenOutdatingService;
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.user.cache.UserAuthDetailsCache;
import java.util.UUID;
@Component
@RequiredArgsConstructor
public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
public class RefreshTokenAuthenticationProvider extends AbstractAuthenticationProvider {
private final JwtTokenFactory tokenFactory;
private final UserAuthDetailsCache userAuthDetailsCache;
private final CustomerService customerService;
private final TokenOutdatingService tokenOutdatingService;
public RefreshTokenAuthenticationProvider(JwtTokenFactory jwtTokenFactory, UserAuthDetailsCache userAuthDetailsCache,
CustomerService customerService, TokenOutdatingService tokenOutdatingService) {
super(customerService, userAuthDetailsCache);
this.tokenFactory = jwtTokenFactory;
this.tokenOutdatingService = tokenOutdatingService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
@ -63,7 +53,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
SecurityUser securityUser;
if (principal.getType() == UserPrincipal.Type.USER_NAME) {
securityUser = authenticateByUserId(unsafeUser.getId());
securityUser = authenticateByUserId(TenantId.SYS_TENANT_ID, unsafeUser.getId());
} else {
securityUser = authenticateByPublicId(principal.getValue());
}
@ -75,52 +65,8 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
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) {
TenantId systemId = TenantId.SYS_TENANT_ID;
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);
return super.authenticateByPublicId(publicId, "Refresh token", null);
}
@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;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
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.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
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.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.UserPrincipal;
import org.thingsboard.server.service.security.model.token.RawApiKey;
import org.thingsboard.server.service.security.model.token.ApiKeyAuthRequest;
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache;
@Component
@RequiredArgsConstructor
public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
public class ApiKeyAuthenticationProvider extends AbstractAuthenticationProvider {
private final ApiKeyService apiKeyService;
private final UserAuthDetailsCache userAuthDetailsCache;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RawApiKey rawApiKey = (RawApiKey) authentication.getCredentials();
SecurityUser securityUser = authenticate(rawApiKey.apiKey());
return new ApiKeyAuthenticationToken(securityUser);
public ApiKeyAuthenticationProvider(ApiKeyService apiKeyService, UserAuthDetailsCache userAuthDetailsCache) {
super(null, userAuthDetailsCache);
this.apiKeyService = apiKeyService;
}
@Override
public boolean supports(Class<?> authentication) {
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ApiKeyAuthRequest apiKeyAuthRequest = (ApiKeyAuthRequest) authentication.getCredentials();
SecurityUser securityUser = authenticate(apiKeyAuthRequest.apiKey());
return new ApiKeyAuthenticationToken(securityUser);
}
private SecurityUser authenticate(String key) {
@ -67,20 +60,12 @@ public class ApiKeyAuthenticationProvider implements org.springframework.securit
if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) {
throw new CredentialsExpiredException("API key is expired");
}
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(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");
}
return super.authenticateByUserId(apiKey.getTenantId(), apiKey.getUserId());
}
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);
@Override
public boolean supports(Class<?> authentication) {
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
}
}

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.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;
@ -26,12 +26,12 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 2978710889397403536L;
private RawApiKey rawApiKey;
private ApiKeyAuthRequest apiKeyAuthRequest;
private SecurityUser securityUser;
public ApiKeyAuthenticationToken(RawApiKey rawApiKey) {
public ApiKeyAuthenticationToken(ApiKeyAuthRequest apiKeyAuthRequest) {
super(null);
this.rawApiKey = rawApiKey;
this.apiKeyAuthRequest = apiKeyAuthRequest;
setAuthenticated(false);
}
@ -44,7 +44,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override
public Object getCredentials() {
return rawApiKey;
return apiKeyAuthRequest;
}
@Override
@ -55,7 +55,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override
public void 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.util.matcher.RequestMatcher;
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;
@ -52,8 +52,9 @@ public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthentic
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
RawApiKey rawApiKey = new RawApiKey(tokenExtractor.extract(request));
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(rawApiKey));
String apiKeyValue = tokenExtractor.extract(request);
ApiKeyAuthRequest apiKeyAuthRequest = new ApiKeyAuthRequest(apiKeyValue);
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(apiKeyAuthRequest));
}
@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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
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.stereotype.Component;
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.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.UserId;
import org.thingsboard.server.common.data.security.Authority;
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.UserPasswordPolicy;
@ -43,6 +37,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
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.MfaConfigurationToken;
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.system.SystemSecurityService;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class RestAuthenticationProvider implements AuthenticationProvider {
public class RestAuthenticationProvider extends AbstractAuthenticationProvider {
private final SystemSecurityService systemSecurityService;
private final SecuritySettingsService securitySettingsService;
private final UserService userService;
private final CustomerService customerService;
private final TwoFactorAuthService twoFactorAuthService;
@Autowired
@ -71,8 +62,8 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
final SystemSecurityService systemSecurityService,
SecuritySettingsService securitySettingsService,
TwoFactorAuthService twoFactorAuthService) {
super(customerService, null);
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.securitySettingsService = securitySettingsService;
this.twoFactorAuthService = twoFactorAuthService;
@ -125,7 +116,6 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
}
try {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
if (userCredentials == null) {
throw new UsernameNotFoundException("User credentials not found");
@ -138,8 +128,9 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
throw e;
}
if (user.getAuthority() == null)
if (user.getAuthority() == null) {
throw new InsufficientAuthenticationException("User has no authority assigned");
}
return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
} catch (Exception e) {
@ -149,28 +140,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
}
private SecurityUser authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
CustomerId customerId;
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);
return super.authenticateByPublicId(publicId, "Public Id", userPrincipal);
}
@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;
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);
long intervalEndTs = customInterval.getCurrentIntervalEndTs();
CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null);
CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null);
long interval = customInterval.getCurrentIntervalDurationMillis();
await().alias("create CF and no telemetry during interval -> save metric with default value")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
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));
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")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
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));
@ -147,9 +149,10 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
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();
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")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
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));
@ -189,13 +193,14 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
.atMost(2 * 10, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption");
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
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<>();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null));
@ -209,10 +214,86 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
consumption.setDefaultValue(9999L);
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();
output.setDecimalsByDefault(0);
return createAggCf("Consumption per minute", entityId,
return createAggCf("CF with 2 args", entityId,
aggInterval,
watermark,
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() {
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, 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.stubbing.Answer;
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.CacheManager;
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.web.MockMultipartFile;
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.web.servlet.MockMvc;
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.rest.LoginRequest;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.system.SystemPatchApplier;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -295,18 +296,21 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
private JwtTokenFactory jwtTokenFactory;
@SpyBean
protected MailService mailService;
@Autowired
protected InMemoryStorage storage;
@Autowired
protected JdbcTemplate jdbcTemplate;
@MockBean
@MockitoSpyBean
protected MailService mailService;
@MockitoBean
protected CfRocksDb cfRocksDb;
@MockitoBean
protected SystemPatchApplier systemPatchApplier;
@Rule
public TestRule watcher = new TestWatcher() {
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.Mockito;
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.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -96,15 +96,15 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
private RuleEngineStatisticsService ruleEngineStatisticsService;
@Autowired
private StatsFactory statsFactory;
@SpyBean
private TimeseriesDao timeseriesDao;
@Autowired
private QueueStatsService queueStatsService;
@SpyBean
@MockitoSpyBean
private TimeseriesDao timeseriesDao;
@MockitoSpyBean
private PartitionService partitionService;
@SpyBean
@MockitoSpyBean
private TimeseriesService timeseriesService;
@SpyBean
@MockitoSpyBean
private ActorSystemContext actorSystemContext;
@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.getDescriptor(ImageDescriptor.class)).isEqualTo(imageDescriptor);
assertThat(newImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(newImageInfo.getPublicResourceKey());
assertThat(newImageInfo.getPublicResourceKey()).isEqualTo(imageInfo.getPublicResourceKey());
}
@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.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.ActorSystemContext;
@ -109,11 +109,11 @@ public class TenantControllerTest extends AbstractControllerTest {
ListeningExecutorService executor;
@SpyBean
@MockitoSpyBean
private PartitionService partitionService;
@SpyBean
@MockitoSpyBean
private ActorSystemContext actorContext;
@SpyBean
@MockitoSpyBean
private TbQueueAdmin queueAdmin;
@Before
@ -269,12 +269,11 @@ public class TenantControllerTest extends AbstractControllerTest {
@Test
public void testFindTenants() throws Exception {
loginSysAdmin();
List<Tenant> tenants = new ArrayList<>();
PageLink pageLink = new PageLink(17);
PageData<Tenant> pageData = doGetTypedWithPageLink("/api/tenants?", PAGE_DATA_TENANT_TYPE_REF, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getData().size());
tenants.addAll(pageData.getData());
List<Tenant> tenants = new ArrayList<>(pageData.getData());
Mockito.reset(tbClusterService);
@ -400,12 +399,11 @@ public class TenantControllerTest extends AbstractControllerTest {
@Test
public void testFindTenantInfos() throws Exception {
loginSysAdmin();
List<TenantInfo> tenants = new ArrayList<>();
PageLink pageLink = new PageLink(17);
PageData<TenantInfo> pageData = doGetTypedWithPageLink("/api/tenantInfos?", PAGE_DATA_TENANT_INFO_TYPE_REF, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getData().size());
tenants.addAll(pageData.getData());
List<TenantInfo> tenants = new ArrayList<>(pageData.getData());
List<ListenableFuture<TenantInfo>> createFutures = new ArrayList<>(56);
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;
@NoXss
@Schema(description = "Api key value", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "API key value", requiredMode = Schema.RequiredMode.REQUIRED)
private String value;
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
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;
@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;
@Schema(description = "Expiration time of the api key.")
@Schema(description = "Expiration time of the API key.")
private long expirationTime;
@NoXss
@NotBlank
@Length(fieldName = "description")
@Schema(description = "Api Key description.", example = "Api Key description")
@Schema(description = "API Key description.", example = "API Key description")
private String description;
@Schema(description = "Enabled/disabled api key.", example = "true")
@Schema(description = "Enabled/disabled API key.", example = "true")
private boolean enabled;
@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",
accessMode = Schema.AccessMode.READ_ONLY)
public boolean isExpired() {
@ -67,10 +67,10 @@ public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId {
return System.currentTimeMillis() > expirationTime;
}
@Schema(description = "JSON object with the Api Key Id. " +
"Specify this field to update the Api Key. " +
"Referencing non-existing Api Key Id will cause error. " +
"Omit this field to create new Api Key.")
@Schema(description = "JSON object with the API Key Id. " +
"Specify this field to update the API Key. " +
"Referencing non-existing API Key Id will cause error. " +
"Omit this field to create new API Key.")
@Override
public ApiKeyId 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.stream.Collectors;
/**
* Created by ashvayka on 13.07.17.
*/
@Data
@MappedSuperclass
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;
/**
* Created by Victor Basanets on 8/30/2017.
*/
@Data
@EqualsAndHashCode(callSuper = true)
@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;
/**
* Created by Valerii Sosliuk on 4/21/2017.
*/
@Data
@EqualsAndHashCode(callSuper = true)
@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_TABLE_NAME;
@Data
@Entity
@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));
return result;
}
}

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

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

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

@ -15,12 +15,12 @@
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { createDefaultHttpOptions, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { TimePageLink } from '@shared/models/page/page-link';
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';
@Injectable({
@ -32,28 +32,58 @@ export class AuditLogService {
private http: HttpClient
) { }
public getAuditLogs(pageLink: TimePageLink,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getAuditLogs(pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogs(pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogs(
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,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByCustomerId(
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,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByUserId(
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,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable<PageData<AuditLog>>;
public getAuditLogsByEntityId(
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/>' +
errorResponse.status + ': ' + errorResponse.statusText);
}
} else if (errorResponse.status === 504) {
if (!ignoreErrors) {
this.showError('Request timeout');
}
} else {
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"
subscriptSizing="dynamic"
showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList"
@ -83,12 +84,14 @@
[entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[watchKeyChange]="true"
[disabledAddButton]="!fieldFormGroup.get('entityId.id').value"
[entityName]="data.entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
<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>
</div>
</div>
@ -97,7 +100,7 @@
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<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>
</div>
<button mat-icon-button
@ -116,10 +119,7 @@
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
(click)="addClearAlarmRule()">
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button>
</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 { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
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 { Observable } from "rxjs";
import { switchMap } from "rxjs/operators";
export interface AlarmRuleDialogData {
value?: CalculatedField;
@ -43,6 +50,7 @@ export interface AlarmRuleDialogData {
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
isDirty?: boolean;
getTestScriptDialogFn: AlarmRuleTestScriptFn,
}
@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;
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter');
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
alarmRuleFilterConfigForm: FormGroup;
@ -281,6 +281,9 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
if (this.alarmRuleFilterConfig?.name?.length) {
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) {
this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
} 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 { AppState } from '@core/core.state';
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 { 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 { MatDialog } from '@angular/material/dialog';
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 { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store';
@ -36,13 +36,17 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import {
ArgumentEntityType,
ArgumentType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldEventArguments,
CalculatedFieldsQuery,
CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
} from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.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";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
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 { 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> {
@ -223,7 +232,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
const copyCalculatedAlarmRule = deepClone(calculatedField);
copyCalculatedAlarmRule.entityId = null;
if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
@ -245,6 +257,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
},
enterAnimationDuration: isDirty ? 0 : null,
})
@ -277,6 +290,19 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
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)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -287,15 +313,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
if (calculatedField.type === CalculatedFieldType.GEOFENCING) {
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 {
if (calculatedField.type === CalculatedFieldType.ALARM) {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
@ -315,4 +333,41 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
takeUntilDestroyed(this.destroyRef),
).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' }"
helpId="alarm-rule/expression_fn">
<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>
<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>
<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>

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

@ -28,6 +28,7 @@ import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
@ -38,11 +39,13 @@ import {
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { Observable } from "rxjs";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmRuleCondition;
arguments?: Record<string, CalculatedFieldArgument>;
testScript: (expression: string) => Observable<string>;
}
@Component({
@ -117,7 +120,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.patchValue({
expression: {
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
expression: this.condition?.expression?.expression ?? null,
expression: this.condition?.expression?.expression ?? alarmRuleDefaultScript,
filters: this.condition?.expression?.filters ?? [],
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);
}
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"
[specText]="specText"
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>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
</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_VALUE_ACCESSOR,
UntypedFormControl,
Validator,
Validators
Validator
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
@ -50,6 +49,7 @@ import {
CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-cf-alarm-rule-condition',
@ -81,6 +81,13 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'],
expression: [{type: AlarmRuleExpressionType.SIMPLE}],
@ -118,10 +125,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
}
writeValue(value: AlarmRuleCondition): void {
if (value) {
this.modelValue = value;
this.updateConditionInfo();
}
this.modelValue = value;
this.updateConditionInfo();
}
public conditionSet() {
@ -147,7 +152,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
data: {
readonly: this.disabled,
condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
arguments: this.arguments
arguments: this.arguments,
testScript: this.testScript
}
}).afterClosed().subscribe((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">
<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>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<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.
*/
:host {
width: 100%;
.tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px;
&.title {

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

@ -34,6 +34,7 @@ import {
AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-cf-alarm-rule',
@ -65,6 +66,13 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
private modelValue: AlarmRule;
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-form-field>
</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>
</div>
<button *ngIf="!disabled"
@ -57,9 +57,7 @@
<button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
(click)="addCreateAlarmRule()">
{{ 'alarm-rule.add-create-alarm-rule' | translate }}
</button>
</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 { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-create-cf-alarm-rules',
@ -60,6 +61,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
@ -157,7 +160,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
}
public validate(c: UntypedFormControl) {
return (this.createAlarmRulesFormArray().length && this.createAlarmRulesFormGroup.valid) ? null : {
return this.createAlarmRulesFormArray().length ? null : {
createAlarmRules: {
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">
<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"
selectMediaBreakpoint="xs">
<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>
<button mat-button mat-raised-button color="primary"
(click)="addFilter()"
type="button"
matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'alarm-rule.add-filter' | translate }}
</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"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'filter.add-complex' | translate }}
</button>
</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,
actionTypeTranslations,
AuditLog,
AuditLogMode
AuditLogMode,
AuditLogFilter
} from '@shared/models/audit-log.models';
import {
AliasEntityType,
@ -48,6 +49,8 @@ import {
AuditLogDetailsDialogComponent,
AuditLogDetailsDialogData
} 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> {
@ -71,6 +74,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.searchEnabled = true;
this.addEnabled = false;
this.entitiesDeleteEnabled = false;
this.headerComponent = AuditLogHeaderComponent;
this.actionsColumnTitle = 'audit-log.details';
this.entityTranslations = {
noEntities: 'audit-log.no-audit-logs-prompt',
@ -79,6 +83,10 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.entityResources = {
} as EntityTypeResource<AuditLog>;
this.componentsData = {
auditLogFilter: []
};
this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink);
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
@ -139,15 +147,16 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
}
fetchAuditLogs(pageLink: TimePageLink): Observable<PageData<AuditLog>> {
const auditLogFilter: AuditLogFilter = deepClone(this.componentsData?.auditLogFilter) || {};
switch (this.auditLogMode) {
case AuditLogMode.TENANT:
return this.auditLogService.getAuditLogs(pageLink);
return this.auditLogService.getAuditLogs(pageLink, auditLogFilter);
case AuditLogMode.ENTITY:
return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink);
return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink, auditLogFilter);
case AuditLogMode.USER:
return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink);
return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink, auditLogFilter);
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 { MatDialog } from '@angular/material/dialog';
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 { EntityId } from '@shared/models/id/entity-id';
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 { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
@ -256,6 +257,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
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)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -295,9 +309,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).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 (
calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
(calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true)
) {
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'],
data: {
arguments: resultArguments,
expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
expression: expression ?? (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
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 class="tb-form-panel no-border no-padding">
@if (!isOutputKey) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
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>
@if (!watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
}
<ng-container>
@if (!hiddenEntityTypes) {
@ -105,7 +98,7 @@
<ng-container [ngTemplateOutlet]="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>
</div>
} @else {
@ -132,6 +125,7 @@
<tb-entity-key-autocomplete
formControlName="key"
class="flex-1"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute"
[entityFilter]="entityFilter"
[keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value"
@ -140,15 +134,8 @@
}
}
</ng-container>
@if (isOutputKey) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
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 (watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
}
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
@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() isScript: boolean;
@Input() usedArgumentNames: string[];
@Input() isOutputKey = false;
@Input() watchKeyChange = false;
@Input() hiddenEntityTypes = false;
@Input() hiddenEntityKeyTypes = false;
@Input() hiddenDefaultValue = false;
@ -80,6 +80,14 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() predefinedEntityFilter: EntityFilter;
@Input() forbiddenNames = FORBIDDEN_NAMES;
@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;
@ -107,6 +115,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
readonly ArgumentType = ArgumentType;
readonly DataKeyType = DataKeyType;
@ -155,7 +165,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType();
this.setInitialEntityType();
this.setWatchKeyChange();
if (this.watchKeyChange) {
this.setWatchKeyChange();
}
if (this.defaultValueRequired) {
this.argumentFormGroup.get('defaultValue').addValidators(Validators.required);
@ -202,6 +214,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private updatedArgumentType(): void {
let argumentType = ArgumentEntityType.Current;
if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) {
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE);
argumentType = ArgumentEntityType.Owner;
} else if (this.argument.refEntityId?.entityType) {
argumentType = this.argument.refEntityId.entityType;
@ -270,6 +283,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
.pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => {
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);
if (!this.enableAttributeScopeSelection) {
this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE);
@ -299,15 +313,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
}
private setWatchKeyChange(): void {
if (this.isOutputKey) {
this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
}
});
}
this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
}
});
}
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>
</table>
<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 }}
</div>
@if (errorText) {
@if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
@ -134,7 +134,7 @@
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton"
>
{{ 'calculated-fields.add-argument' | translate }}
</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() ownerId: EntityId;
@Input() isScript: boolean;
@Input() disabledAddButton = false;
@Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -181,6 +183,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
watchKeyChange: this.watchKeyChange,
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
};
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.panelAdditionalCtx = {
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'],
};
}

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

@ -88,6 +88,7 @@
[entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component>
}
@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;
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, args, false);
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression);
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
}
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>
<tb-calculated-field-metrics-table formControlName="metrics"
simpleMode
[testScript]="testScript"
[arguments]="arguments$ | async"
></tb-calculated-field-metrics-table>
</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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { merge, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import _moment from 'moment';
@ -85,6 +85,8 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
@Input({required: true})
entityName: string;
@Input() testScript: (expression?: string) => Observable<string>;
readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF;
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>
</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="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
@ -204,6 +204,7 @@
</mat-form-field>
} @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter"
[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;
entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityType = ArgumentEntityType;
readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[];
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
@ -151,6 +153,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
} else {
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.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type);
@ -265,18 +269,17 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
)
.pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType));
this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset(''));
}
private observeEntityTypeChanges(): void {
this.refEntityIdFormGroup.get('entityType').valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed())
.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);
const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery;
this.geofencingFormGroup.get('refEntityId')
.get('id')[isEntityWithId ? 'enable' : 'disable']();
this.geofencingFormGroup.get('perimeterKeyName').reset('');
const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type);
this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) {
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
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-required' | translate"
class="tb-error">
warning
</mat-icon>
@ -35,7 +35,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-duplicate' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-duplicate' | translate"
class="tb-error">
warning
</mat-icon>
@ -43,7 +43,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
@ -51,7 +51,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
@ -59,7 +59,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
@ -105,7 +105,24 @@
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</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>
<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>
</mat-expansion-panel>
</div>
@ -146,7 +163,7 @@
} @else {
<tb-js-func required
formControlName="function"
functionName="filter"
functionName="map"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
@ -157,7 +174,24 @@
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</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>
<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>
@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 { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue {
allowFilter: boolean;
@ -52,6 +53,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
@Input() simpleMode: boolean;
@Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
metricDataApplied = output<CalculatedFieldAggMetricValue>();
filterExpanded = false;
@ -81,7 +83,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
constructor(
private fb: FormBuilder,
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>,
) {
this.observeFilterAllowChange();
this.observeInputTypeChange();
@ -163,4 +165,11 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
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';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
@Component({
selector: 'tb-calculated-field-metrics-table',
@ -80,6 +81,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
@Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules;
@Input({transform: booleanAttribute}) simpleMode: boolean = false;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -166,6 +168,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
editorCompleter: this.editorCompleter,
highlightRules: this.highlightRules,
simpleMode: this.simpleMode,
testScript: this.testScript
};
this.popoverComponent = this.popoverService.displayPopover({
trigger,

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

@ -46,7 +46,7 @@
@if (hiddenName) {
<div class="grid items-start gap-3 has-[.simpleMode]:grid-cols-2 xs:grid-cols-1">
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
<ng-content select=".simpleMode"></ng-content>
<ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
</div>
} @else {
<div class="flex items-start gap-3 xs:flex-col xs:items-stretch">
@ -74,93 +74,94 @@
</mat-form-field>
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
</div>
<ng-content select=".simpleMode"></ng-content>
<ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
}
}
</div>
<div class="tb-form-panel stroked" formGroupName="strategy">
<div class="flex flex-row items-center justify-between gap-2">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
<mat-expansion-panel class="tb-settings" [disabled]="outputForm.get('strategy.type').value === OutputStrategyType.RULE_CHAIN">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<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 }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</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>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-normal" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.processing-options' | translate }}">
{{ 'calculated-fields.output-strategy.processing-options' | translate }}
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.send-web-sockets' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div>
</div>
</mat-slide-toggle>
</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">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
<mat-slide-toggle class="mat-slide" formControlName="processCfs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-calculated-fields' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<tb-time-unit-input
required
subscriptSizing="dynamic"
appearance="outline"
sameWidthInputs
labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}"
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}"
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"
formControlName="ttl">
<mat-icon class="help-icon mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
help_outline
</mat-icon>
</tb-time-unit-input>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="useCustomTtl">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.ttl</div>
</div>
</mat-slide-toggle>
<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>
<ng-template #decimalsByDefaultField>
@ -172,3 +173,6 @@
}
</mat-form-field>
</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 { EntityType } from '@shared/models/entity-type.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { merge } from 'rxjs';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-calculate-field-output',
@ -102,6 +104,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
sendWsUpdate: [true],
processCfs: [true],
updateAttributesOnlyOnValueChange: [true],
useCustomTtl: [false],
ttl: [0]
})
});
@ -116,7 +119,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
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))
.subscribe(() => {
this.updatedStrategy();
@ -151,6 +157,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void {
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});
}
@ -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) {
if (this.simpleMode && 'name' in value) {
value.name = value.name?.trim() ?? '';
@ -185,6 +187,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (this.disableType) {
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);
}
@ -227,7 +233,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
} else {
this.outputForm.get('strategy.saveTimeSeries').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 }}
</div>
<tb-calculated-field-metrics-table formControlName="metrics"
[testScript]="testScript"
[arguments]="arguments$ | async"
[highlightRules]="argumentsHighlightRules$ | 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})
entityName: string;
@Input({required: true})
testScript: (expression?: string) => Observable<string>;
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
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 { PageComponent } from '@shared/components/page.component';
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 { PageLink } from '@shared/models/page/page-link';
@ -41,8 +39,8 @@ export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>,
return this.entitiesTableConfigValue;
}
protected constructor(protected store: Store<AppState>) {
super(store);
protected constructor(...args: unknown[]) {
super();
}
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 { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit-log-details-dialog.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 { EventTableComponent } from '@home/components/event/event-table.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 { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-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({
declarations:
@ -358,6 +360,8 @@ import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-t
AddApiKeyDialogComponent,
EditApiKeyDescriptionPanelComponent,
ApiKeyGeneratedDialogComponent,
AuditLogHeaderComponent,
AuditLogFilterComponent
],
imports: [
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) => {
const data = formattedDataFormDatasourceData(subscription.data);
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.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.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
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 = this.isFiniteNumber(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
});
this.cd.detectChanges();
}
@ -114,6 +114,10 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
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) {
$event?.preventDefault();
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>
<tb-entity-alias-select
class="tb-entity-alias-field"
showLabel
tbRequired
[appearance]="'outline'"
[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">
{{ 'widgets.table.use-entity-label-tab-name' | translate }}
</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>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tabSortKey" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="TabSortKey.TIMESTAMP">{{ 'widgets.table.sort-timestamp-option' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_ASC">{{ 'widgets.table.sort-asc' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_DESC">{{ 'widgets.table.sort-desc' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-row justify-end gap-2 ">
<mat-form-field class="flex-lt-lg medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="property" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="entityFields.createdTime.value">{{ entityFields.createdTime.name | translate }}</mat-option>
<mat-option [value]="entityFields.name.value">{{ entityFields.name.name | translate }}</mat-option>
</mat-select>
</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 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 { AppState } from '@core/core.state';
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({
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 {
TabSortKey = TabSortKey;
entityFields = entityFields;
Direction = Direction;
timeseriesTableWidgetSettingsForm: UntypedFormGroup;
pageStepSizeValues = [];
@ -62,13 +64,19 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: false,
useRowStyleFunction: false,
rowStyleFunction: '',
tabSortKey: TabSortKey.TIMESTAMP
sortOrder: {
property: this.entityFields.createdTime.keyName,
direction: Direction.DESC
}
};
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
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);
return settings;
}
@ -99,7 +107,16 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: [settings.disableStickyHeader, []],
useRowStyleFunction: [settings.useRowStyleFunction, []],
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'];
}
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) {
if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') {
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;
@Input()
@coerceBoolean()
showLabel: boolean;
@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 { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models';
export enum TabSortKey {
NAME_ASC = 'NAME_ASC',
NAME_DESC = 'NAME_DESC',
TIMESTAMP = 'timestamp'
}
import { entityFields } from '@shared/models/entity.models';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean;
showMilliseconds: boolean;
hideEmptyLines: boolean;
dateFormat: DateFormatSettings;
tabSortKey: TabSortKey;
sortOrder: SortOrder;
}
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
@ -400,33 +395,39 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.updateDatasources();
}
private getTabLabel(source: Datasource, entityLabelCache:Map<string,string>):string {
if (entityLabelCache.has(source.entityId)) {
return entityLabelCache.get(source.entityId);
}
const value = this.useEntityLabel
private getTabLabel(source: Datasource):string {
const value = this.useEntityLabel
? (source.entityLabel || source.entityName)
: source.entityName;
const translated = this.utils.customTranslation(value);
entityLabelCache.set(source.entityId, translated);
return translated;
return this.utils.customTranslation(value);
}
private sortDatasources(source: Datasource[], entityLabelCache:Map<string,string>): Datasource[] {
source.forEach(ds => this.getTabLabel(ds, entityLabelCache));
private sortDatasources(source: TimeseriesTableSource[]) {
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 source;
return isAsc
? 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() {
@ -434,11 +435,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sourceIndex = 0;
let keyOffset = 0;
let latestKeyOffset = 0;
const entityLabelCache = new Map<string,string>();
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
if (this.datasources) {
const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache);
for (const datasource of sortedDatasources) {
for (const datasource of this.datasources) {
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
const source = {} as TimeseriesTableSource;
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.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null;
source.displayName = entityLabelCache.get(datasource.entityId);
source.displayName = this.getTabLabel(datasource);
if (this.showTimestamp) {
source.displayedColumns.push('0');
}
@ -475,6 +474,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
}
if (this.sources.length) {
this.sortDatasources(this.sources);
this.sources.forEach((source, index) => {
this.prepareDisplayedColumn(index);
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>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<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 #alarmRules="matTab"
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 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 { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { AssetInfo } from '@app/shared/models/asset.models';
import { EntityId } from "@shared/models/id/entity-id";
@Component({
selector: 'tb-asset-tabs',
@ -27,6 +28,8 @@ import { AssetInfo } from '@app/shared/models/asset.models';
})
export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) {
super(store);
}
@ -35,4 +38,9 @@ export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
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';
@NgModule({
declarations: [
],
declarations: [],
imports: [
CommonModule,
SharedModule,

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

@ -33,10 +33,10 @@
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<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 #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 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 { DeviceInfo } from '@shared/models/device.models';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { EntityId } from "@shared/models/id/entity-id";
@Component({
selector: 'tb-device-tabs',
@ -27,6 +28,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone
})
export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) {
super(store);
}
@ -35,4 +38,9 @@ export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
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="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ 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>
</section>
</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-form-field>
</div>
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
<mat-form-field class="max-w-30% flex-full xs:max-w-full">
<div class="grid gt-sm:grid-cols-3 gt-sm:gap-4">
<mat-form-field class="mat-block">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes"
@ -54,32 +54,28 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1"
*ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
class="max-w-35% flex-full xs:max-w-full"
required
formControlName="dashboardId">
@if(actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK) {
<mat-form-field class="gt-sm:col-span-2">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
} @else {
<tb-dashboard-autocomplete required formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete class="max-w-35% flex-full xs:max-w-full"
[dashboardId]="actionButtonConfigForm.get('dashboardId').value"
<tb-dashboard-state-autocomplete [dashboardId]="actionButtonConfigForm.get('dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
}
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*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) {
<mat-option [value]="key"><span [innerHTML]="key | highlight: searchText"></span></mat-option>
} @empty {
@if (!this.keyControl.value) {
@if (!this.keyControl.value && enableAutocomplete) {
<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() requiredText = this.translate.instant('common.hint.key-required');
@Input() enableAutocomplete = true;
entityFilter = input.required<EntityFilter>();
dataKeyType = input.required<DataKeyType>();
@ -81,12 +82,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
keys$ = this.keyInputSubject.asObservable()
.pipe(
switchMap(() => {
if (!this.enableAutocomplete) {
return of([] as string[]);
}
return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({
pageLink: { page: 0, pageSize: 100 },
entityFilter: this.entityFilter(),
}, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true});
}),
map(result => {
if (Array.isArray(result)) {
return result;
}
this.cachedResult = result;
switch (this.dataKeyType()) {
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:
this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-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.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty';
break;

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

Loading…
Cancel
Save