Browse Source

Merge branch 'master' into feature/cf-merge-function

feature/cf-merge-function
Andrii Shvaika 2 years ago
parent
commit
0ba3352a04
  1. 2
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  2. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 5
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 30
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  5. 9
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  8. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java
  10. 82
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java
  11. 98
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  13. 11
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  14. 5
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  15. 10
      application/src/main/resources/thingsboard.yml
  16. 134
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  17. 74
      application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
  18. 202
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  19. 49
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  20. 27
      application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
  21. 1
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  22. 24
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  23. 84
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  24. 144
      application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java
  25. 123
      application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
  26. 221
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  27. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  28. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
  29. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java
  30. 9
      common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java
  31. 32
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
  32. 26
      common/edge-api/src/main/proto/edge.proto
  33. 5
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  34. 1
      common/proto/src/main/proto/queue.proto
  35. 56
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java
  36. 38
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  37. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  38. 60
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
  39. 11
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  40. 29
      dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
  41. 3
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java
  42. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java
  43. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  44. 12
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  45. 60
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  46. 11
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  47. 15
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  48. 20
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  49. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
  50. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
  51. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  52. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
  53. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  54. 1
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  55. 5
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  56. 1
      dao/src/main/resources/sql/schema-entities.sql
  57. 47
      dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java
  58. 2
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java
  59. 6
      monitoring/src/main/resources/lwm2m/device_profile.json
  60. 324
      monitoring/src/main/resources/lwm2m/models/test-model.xml
  61. 2
      monitoring/src/main/resources/lwm2m/resource.json
  62. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  63. 2
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  64. 2
      pom.xml
  65. 16
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  66. 8
      transport/coap/src/main/resources/tb-coap-transport.yml
  67. 4
      transport/http/src/main/resources/tb-http-transport.yml
  68. 4
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  69. 4
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  70. 4
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  71. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  72. 2
      ui-ngx/src/app/core/auth/auth.reducer.ts
  73. 4
      ui-ngx/src/app/core/http/resource.service.ts
  74. 17
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  75. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  76. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  77. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  78. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss
  79. 17
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  80. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss
  81. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  82. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  83. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss
  84. 92
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  85. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  86. 5
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  87. 191
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  88. 130
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts
  89. 68
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  90. 10
      ui-ngx/src/app/shared/components/image/image-references.component.ts
  91. 42
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html
  92. 2
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss
  93. 61
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts
  94. 156
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  95. 24
      ui-ngx/src/app/shared/models/resource.models.ts
  96. 15
      ui-ngx/src/app/shared/models/tenant.model.ts
  97. 6
      ui-ngx/src/app/shared/shared.module.ts
  98. 1
      ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md
  99. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json

@ -119,7 +119,7 @@
"type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"name": "Push to cloud",
"configuration": {
"scope": "SERVER_SCOPE"
"scope": "CLIENT_SCOPE"
},
"externalId": null
},

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

@ -62,3 +62,5 @@ DO $$
$$;
-- UPDATE SAVE TIME SERIES NODES END
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;

5
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -179,7 +179,7 @@ public class DefaultTbContext implements TbContext {
.resetRuleNodeId()
.build();
tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId());
TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator());
TopicPartitionInfo tpi = resolvePartition(msg);
doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t)));
}
@ -196,8 +196,7 @@ public class DefaultTbContext implements TbContext {
@Override
public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer<Throwable> onFailure) {
TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator());
enqueue(tpi, tbMsg, onFailure, onSuccess);
enqueue(tbMsg, tbMsg.getQueueName(), onSuccess, onFailure);
}
@Override

30
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -35,19 +35,22 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldScriptEngine;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
@ -61,6 +64,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
@ -84,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class CalculatedFieldController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String CALCULATED_FIELD_ID = "calculatedFieldId";
@ -176,14 +181,31 @@ public class CalculatedFieldController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedField) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedField);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedField));
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.DELETE);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
tbCalculatedFieldService.delete(calculatedField, getCurrentUser());
}
@ApiOperation(value = "Get latest calculated field debug event (getLatestCalculatedFieldDebugEvent)",
notes = "Gets latest calculated field debug event for specified calculated field id. " +
"Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}/debug", method = RequestMethod.GET)
@ResponseBody
public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.READ);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
TenantId tenantId = getCurrentUser().getTenantId();
return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1))
.flatMap(events -> events.stream().map(EventInfo::getBody).findFirst())
.orElse(null);
}
@ApiOperation(value = "Test Script expression",
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")

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

@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -322,12 +323,14 @@ public class TbResourceController extends BaseController {
notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(value = "/resource/{resourceId}")
public void deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable("resourceId") String strResourceId) throws ThingsboardException {
public ResponseEntity<TbResourceDeleteResult> deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable("resourceId") String strResourceId,
@RequestParam(name = "force", required = false) boolean force) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
TbResource tbResource = checkResourceId(resourceId, Operation.DELETE);
tbResourceService.delete(tbResource, getCurrentUser());
TbResourceDeleteResult tbResourceDeleteResult = tbResourceService.delete(tbResource, force, getCurrentUser());
return (tbResourceDeleteResult.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(tbResourceDeleteResult);
}
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(String strResourceId, String etag) throws ThingsboardException {

3
application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java

@ -40,7 +40,8 @@ public abstract class BaseApiUsageState {
private final Map<ApiUsageRecordKey, Long> gaugesReportCycles = new HashMap<>();
@Getter
private final ApiUsageState apiUsageState;
@Setter
private ApiUsageState apiUsageState;
@Getter
private volatile long currentCycleTs;
@Getter

3
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -343,7 +343,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
private void persistAndNotify(BaseApiUsageState state, Map<ApiFeature, ApiUsageStateValue> result) {
log.info("[{}] Detected update of the API state for {}: {}", state.getEntityId(), state.getEntityType(), result);
apiUsageStateService.update(state.getApiUsageState());
ApiUsageState updatedState = apiUsageStateService.update(state.getApiUsageState());
state.setApiUsageState(updatedState);
long ts = System.currentTimeMillis();
List<TsKvEntry> stateTelemetry = new ArrayList<>();
result.forEach((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name()))));

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

@ -74,6 +74,8 @@ import org.thingsboard.server.gen.edge.v1.RequestMsgType;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ResponseMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
@ -820,6 +822,16 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg));
}
}
if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) {
for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) {
result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg));
}
}
if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) {
for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) {
result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg));
}
}
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg));

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java

@ -49,8 +49,12 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor {
@Override
public ListenableFuture<Void> processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
try {
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
if (edgeId.equals(originatorEdgeId)) {
return Futures.immediateFuture(null);
}
switch (actionType) {
case ASSIGNED_TO_CUSTOMER: {
CustomerId customerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), CustomerId.class);

82
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.rule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.function.Function;
@Slf4j
public class BaseRuleChainProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<RuleChain> ruleChainValidator;
protected Pair<Boolean, Boolean> saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) {
boolean created = false;
RuleChain ruleChainFromDb = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId);
if (ruleChainFromDb == null) {
created = true;
}
RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
if (ruleChain == null) {
throw new RuntimeException("[{" + tenantId + "}] ruleChainUpdateMsg {" + ruleChainUpdateMsg + "} cannot be converted to rule chain");
}
boolean isRoot = ruleChain.isRoot();
if (RuleChainType.CORE.equals(ruleChainType)) {
ruleChain.setRoot(false);
} else {
ruleChain.setRoot(ruleChainFromDb == null ? false : ruleChainFromDb.isRoot());
}
ruleChain.setType(ruleChainType);
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
if (created) {
ruleChain.setId(ruleChainId);
}
edgeCtx.getRuleChainService().saveRuleChain(ruleChain, true, false);
return Pair.of(created, isRoot);
}
protected void saveOrUpdateRuleChainMetadata(TenantId tenantId, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
RuleChainMetaData ruleChainMetadata = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
if (ruleChainMetadata == null) {
throw new RuntimeException("[{" + tenantId + "}] ruleChainMetadataUpdateMsg {" + ruleChainMetadataUpdateMsg + "} cannot be converted to rule chain metadata");
}
if (!ruleChainMetadata.getNodes().isEmpty()) {
ruleChainMetadata.setVersion(null);
for (RuleNode ruleNode : ruleChainMetadata.getNodes()) {
ruleNode.setRuleChainId(null);
ruleNode.setId(null);
}
edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true);
}
}
}

98
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java

@ -15,29 +15,123 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.rule;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY;
@Slf4j
@Component
@TbCoreComponent
public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
public class RuleChainEdgeProcessor extends BaseRuleChainProcessor {
public ListenableFuture<Void> processRuleChainMsgFromEdge(TenantId tenantId, Edge edge, RuleChainUpdateMsg ruleChainUpdateMsg) {
log.trace("[{}] executing processRuleChainMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainUpdateMsg, edge.getName());
RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (ruleChainUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
return saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge);
case ENTITY_DELETED_RPC_MESSAGE:
RuleChain ruleChainToDelete = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId);
if (ruleChainToDelete != null) {
edgeCtx.getRuleChainService().unassignRuleChainFromEdge(tenantId, ruleChainId, edge.getId(), false);
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(ruleChainUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed rule chains violated {}", tenantId, ruleChainUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
private ListenableFuture<Void> saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) {
try {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE);
Boolean created = resultPair.getFirst();
if (created) {
createRelationFromEdge(tenantId, edge.getId(), ruleChainId);
pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity());
edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId());
}
Boolean isRoot = resultPair.getSecond();
if (isRoot) {
edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edge.getId());
edgeCtx.getEdgeService().setEdgeRootRuleChain(tenantId, edge, ruleChainId);
}
} catch (Exception e) {
log.error("Failed to save or update rule chain", e);
return Futures.immediateFailedFuture(e);
}
return Futures.immediateFuture(null);
}
private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId, String ruleChainAsString) {
try {
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null);
pushEntityEventToRuleEngine(tenantId, ruleChainId, null, TbMsgType.ENTITY_CREATED, ruleChainAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push rule chain action to rule engine: {}", tenantId, ruleChainId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
public ListenableFuture<Void> processRuleChainMetadataMsgFromEdge(TenantId tenantId, Edge edge, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
log.trace("[{}] executing processRuleChainMetadataMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainMetadataUpdateMsg, edge.getName());
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (ruleChainMetadataUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateRuleChainMetadata(tenantId, ruleChainMetadataUpdateMsg);
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(ruleChainMetadataUpdateMsg.getMsgType());
}
} catch (Exception e) {
String errMsg = String.format("Can't process rule chain metadata update msg %s", ruleChainMetadataUpdateMsg);
log.error(errMsg, e);
return Futures.immediateFailedFuture(new RuntimeException(errMsg, e));
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {

5
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -52,6 +52,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.dao.edge.EdgeSynchronizationManager;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@ -67,6 +68,7 @@ public class EntityStateSourcingListener {
private final TenantService tenantService;
private final TbClusterService tbClusterService;
private final EdgeSynchronizationManager edgeSynchronizationManager;
@PostConstruct
public void init() {
@ -270,6 +272,9 @@ public class EntityStateSourcingListener {
private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) {
if (entity instanceof Edge) {
if (entityId.equals(edgeSynchronizationManager.getEdgeId().get())) {
return;
}
tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent));
} else if (entity instanceof EdgeEvent edgeEvent) {
tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId()));

11
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -89,7 +90,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
}
@Override
public void delete(TbResource tbResource, User user) {
public TbResourceDeleteResult delete(TbResourceInfo tbResource, boolean force, User user) {
if (tbResource.getResourceType() == ResourceType.IMAGE) {
throw new IllegalArgumentException("Image resource type is not supported");
}
@ -97,8 +98,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
TbResourceId resourceId = tbResource.getId();
TenantId tenantId = tbResource.getTenantId();
try {
resourceService.deleteResource(tenantId, resourceId);
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
TbResourceDeleteResult result = resourceService.deleteResource(tenantId, resourceId, force);
if (result.isSuccess()) {
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
}
return result;
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
actionType, user, e, resourceId.toString());

5
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -18,8 +18,9 @@ package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@ -37,7 +38,7 @@ public interface TbResourceService {
TbResourceInfo save(TbResource entity, SecurityUser user) throws Exception;
void delete(TbResource entity, User user);
TbResourceDeleteResult delete(TbResourceInfo entity, boolean force, User user);
List<LwM2mObject> findLwM2mObject(TenantId tenantId,
String sortOrder,

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

@ -1659,6 +1659,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -1734,6 +1736,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine
@ -1754,6 +1758,8 @@ queue:
event_topic: "${TB_QUEUE_CF_EVENT_TOPIC:tb_cf_event}"
# Topic name for Calculated Field (CF) compacted states
state_topic: "${TB_QUEUE_CF_STATE_TOPIC:tb_cf_state}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}"
# Interval in milliseconds to poll messages by CF (Rule Engine) microservices
poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}"
# Amount of partitions used by CF microservices
@ -1772,6 +1778,10 @@ queue:
edge:
# Default topic name
topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}"
# For edge events messages
event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}"
# Amount of partitions used by Edge services
partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}"
# Poll interval for topics related to Edge services

134
application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java

@ -98,6 +98,132 @@ class DefaultTbContextTest {
defaultTbContext = new DefaultTbContext(mainCtxMock, "Test rule chain name", nodeCtxMock);
}
@MethodSource
@ParameterizedTest
public void givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi(String queueName) {
// GIVEN
var tpi = resolve(queueName);
given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi);
var clusterService = mock(TbClusterService.class);
given(mainCtxMock.getClusterService()).willReturn(clusterService);
var callbackMock = mock(TbMsgCallback.class);
given(callbackMock.isMsgValid()).willReturn(true);
var ruleNode = new RuleNode(RULE_NODE_ID);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(TENANT_ID)
.queueName(queueName)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_STRING)
.callback(callbackMock)
.build();
var ruleChainId = new RuleChainId(UUID.randomUUID());
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
given(nodeCtxMock.getSelf()).willReturn(ruleNode);
// WHEN
defaultTbContext.input(msg, ruleChainId);
// THEN
then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any());
}
@MethodSource
@ParameterizedTest
public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) {
// GIVEN
var tpi = resolve(queueName);
given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi);
var clusterService = mock(TbClusterService.class);
given(mainCtxMock.getClusterService()).willReturn(clusterService);
var callbackMock = mock(TbMsgCallback.class);
given(callbackMock.isMsgValid()).willReturn(true);
var ruleNode = new RuleNode(RULE_NODE_ID);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(TENANT_ID)
.queueName(queueName)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_STRING)
.callback(callbackMock)
.build();
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
// WHEN
defaultTbContext.enqueue(msg, () -> {}, t -> {});
// THEN
then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any());
}
@MethodSource
@ParameterizedTest
public void givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) {
// GIVEN
var tpi = resolve(queueName);
given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi);
var clusterService = mock(TbClusterService.class);
given(mainCtxMock.getClusterService()).willReturn(clusterService);
var callbackMock = mock(TbMsgCallback.class);
given(callbackMock.isMsgValid()).willReturn(true);
var ruleNode = new RuleNode(RULE_NODE_ID);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(TENANT_ID)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_STRING)
.callback(callbackMock)
.build();
ruleNode.setRuleChainId(RULE_CHAIN_ID);
ruleNode.setDebugSettings(DebugSettings.failures());
given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID);
// WHEN
defaultTbContext.enqueue(msg, queueName, () -> {}, t -> {});
// THEN
then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any());
}
private static Stream<String> givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() {
return testQueueNames();
}
private static Stream<String> givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() {
return testQueueNames();
}
private static Stream<String> givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() {
return testQueueNames();
}
private static Stream<String> testQueueNames() {
return Stream.of("Main", "Test", null);
}
private TopicPartitionInfo resolve(String queueName) {
var tpiBuilder = TopicPartitionInfo.builder()
.topic(queueName == null ? "MainQueueTopic" : queueName + "QueueTopic")
.partition(1)
.myPartition(true);
return tpiBuilder.build();
}
@Test
public void givenDebugFailuresEvents_whenTellSuccess_thenVerifyDebugOutputNotPersisted() {
// GIVEN
@ -810,10 +936,10 @@ class DefaultTbContextTest {
@MethodSource
@ParameterizedTest
void givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(boolean debugFailures,
long debugAllUntil,
String connection,
boolean shouldPersist,
boolean shouldPersistAfterDurationTime) {
long debugAllUntil,
String connection,
boolean shouldPersist,
boolean shouldPersistAfterDurationTime) {
// GIVEN
var callbackMock = mock(TbMsgCallback.class);
var msg = getTbMsgWithCallback(callbackMock);

74
application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java

@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
@ -60,6 +61,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE;
@ -79,6 +81,8 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Autowired
private DeviceProfileDao deviceProfileDao;
static final String LWM2M_PROFILE_JSON = "{\"name\":\"lwm2m profile\",\"type\":\"DEFAULT\",\"image\":null,\"defaultQueueName\":null,\"transportType\":\"LWM2M\",\"provisionType\":\"DISABLED\",\"description\":\"\",\"profileData\":{\"configuration\":{\"type\":\"DEFAULT\"},\"transportConfiguration\":{\"observeAttr\":{\"observe\":[],\"attribute\":[],\"telemetry\":[\"/11_1.1/0/0\"],\"keyName\":{\"/11_1.1/0/0\":\"profileName\"},\"attributeLwm2m\":{}},\"bootstrap\":[{\"shortServerId\":123,\"bootstrapServerIs\":false,\"host\":\"0.0.0.0\",\"port\":5685,\"clientHoldOffTime\":1,\"serverPublicKey\":\"\",\"serverCertificate\":\"\",\"bootstrapServerAccountTimeout\":0,\"lifetime\":300,\"defaultMinPeriod\":1,\"notifIfDisabled\":true,\"binding\":\"U\",\"securityMode\":\"NO_SEC\"}],\"clientLwM2mSettings\":{\"clientOnlyObserveAfterConnect\":1,\"fwUpdateStrategy\":1,\"swUpdateStrategy\":1,\"powerMode\":\"DRX\",\"edrxCycle\":81000,\"psmActivityTimer\":10000,\"pagingTransmissionWindow\":10000,\"defaultObjectIDVer\":\"1.0\"},\"bootstrapServerUpdateEnable\":false,\"type\":\"LWM2M\"},\"alarms\":null,\"provisionConfiguration\":{\"type\":\"DISABLED\"}}}";
static class Config {
@Bean
@Primary
@ -119,7 +123,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Assert.assertNotNull(savedDeviceProfile);
Assert.assertNotNull(savedDeviceProfile.getId());
Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0);
@ -135,7 +139,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
ActionType.ADDED);
savedDeviceProfile.setName("New device profile");
doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class);
saveDeviceProfile(savedDeviceProfile);
DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName());
@ -162,7 +166,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testFindDeviceProfileById() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
Assert.assertNotNull(foundDeviceProfile);
Assert.assertEquals(savedDeviceProfile, foundDeviceProfile);
@ -171,7 +175,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void whenGetDeviceProfileById_thenPermissionsAreChecked() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
deviceProfile = saveDeviceProfile(deviceProfile);
loginDifferentTenant();
@ -183,7 +187,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testFindDeviceProfileInfoById() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/" + savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class);
Assert.assertNotNull(foundDeviceProfileInfo);
Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId());
@ -213,7 +217,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
deviceProfile = saveDeviceProfile(deviceProfile);
loginDifferentTenant();
doGet("/api/deviceProfileInfo/" + deviceProfile.getId())
@ -235,7 +239,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testSetDefaultDeviceProfile() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Mockito.reset(tbClusterService, auditLogService);
@ -328,7 +332,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testChangeDeviceProfileTypeNull() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Mockito.reset(tbClusterService, auditLogService);
@ -345,7 +349,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Device device = new Device();
device.setName("Test device");
device.setType("default");
@ -367,7 +371,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testDeleteDeviceProfileWithExistingDevice() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Device device = new Device();
device.setName("Test device");
@ -419,7 +423,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
public void testSaveDeviceProfileWithFirmwareFromDifferentTenant() throws Exception {
loginDifferentTenant();
DeviceProfile differentProfile = createDeviceProfile("Different profile");
differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class);
differentProfile = saveDeviceProfile(differentProfile);
SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest();
firmwareInfo.setDeviceProfileId(differentProfile.getId());
firmwareInfo.setType(FIRMWARE);
@ -441,7 +445,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
public void testSaveDeviceProfileWithSoftwareFromDifferentTenant() throws Exception {
loginDifferentTenant();
DeviceProfile differentProfile = createDeviceProfile("Different profile");
differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class);
differentProfile = saveDeviceProfile(differentProfile);
SaveOtaPackageInfoRequest softwareInfo = new SaveOtaPackageInfoRequest();
softwareInfo.setDeviceProfileId(differentProfile.getId());
softwareInfo.setType(SOFTWARE);
@ -462,7 +466,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testDeleteDeviceProfile() throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Mockito.reset(tbClusterService, auditLogService);
@ -495,7 +499,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
int cntEntity = 28;
for (int i = 0; i < cntEntity; i++) {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i);
deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
deviceProfiles.add(saveDeviceProfile(deviceProfile));
}
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new DeviceProfile(), new DeviceProfile(),
@ -552,7 +556,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
for (int i = 0; i < 28; i++) {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i);
deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
deviceProfiles.add(saveDeviceProfile(deviceProfile));
}
List<DeviceProfileInfo> loadedDeviceProfileInfos = new ArrayList<>();
@ -961,7 +965,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
JsonTransportPayloadConfiguration jsonTransportPayloadConfiguration = new JsonTransportPayloadConfiguration();
MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(jsonTransportPayloadConfiguration, true);
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration);
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Assert.assertNotNull(savedDeviceProfile);
Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT);
Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration);
@ -979,7 +983,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
"v1/devices/me/telemetry", "v1/devices/me/attributes", "v1/devices/me/subscribeattributes");
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile",
mqttDeviceProfileTransportConfiguration);
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Assert.assertNotNull(savedDeviceProfile);
Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT);
Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration);
@ -997,7 +1001,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema, null, null);
MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration, false);
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration);
DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile);
Assert.assertNotNull(savedDeviceProfile);
DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
Assert.assertEquals(savedDeviceProfile, foundDeviceProfile);
@ -1036,14 +1040,14 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
@Test
public void testDeleteDeviceProfileWithDeleteRelationsOk() throws Exception {
DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelationsOk").getId();
DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelationsOk").getId();
testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId);
}
@Ignore
@Test
public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception {
DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId();
DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId);
}
@ -1103,8 +1107,36 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(count, deviceProfileNames.size());
}
private DeviceProfile savedDeviceProfile(String name) {
@Test
public void testSaveDeviceProfileWithOutdatedVersion() throws Exception {
DeviceProfile deviceProfile = JacksonUtil.fromString(LWM2M_PROFILE_JSON, DeviceProfile.class);
deviceProfile.setName("Device profile v1.0");
deviceProfile = saveDeviceProfile(deviceProfile);
assertThat(deviceProfile.getVersion()).isOne();
deviceProfile.setName("Device profile v2.0");
deviceProfile = saveDeviceProfile(deviceProfile);
assertThat(deviceProfile.getVersion()).isEqualTo(2);
deviceProfile.setName("Device profile v1.1");
deviceProfile.setVersion(1L);
String response = doPost("/api/deviceProfile", deviceProfile).andExpect(status().isConflict())
.andReturn().getResponse().getContentAsString();
assertThat(JacksonUtil.toJsonNode(response).get("message").asText())
.containsIgnoringCase("already changed by someone else");
deviceProfile.setVersion(null); // overriding entity
deviceProfile = saveDeviceProfile(deviceProfile);
assertThat(deviceProfile.getName()).isEqualTo("Device profile v1.1");
assertThat(deviceProfile.getVersion()).isEqualTo(3);
}
private DeviceProfile saveDeviceProfile(String name) {
DeviceProfile deviceProfile = createDeviceProfile(name);
return saveDeviceProfile(deviceProfile);
}
private DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
}

202
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

File diff suppressed because one or more lines are too long

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

@ -90,14 +90,12 @@ import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
@ -142,35 +140,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.expectMessageAmount(25);
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
edgeImitator.expectMessageAmount(26);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
verifyEdgeConnectionAndInitialData();
}
private void requestEdgeRuleChainMetadata() throws Exception {
RuleChainId rootRuleChainId = getEdgeRootRuleChainId();
RuleChainMetadataRequestMsg.Builder builder = RuleChainMetadataRequestMsg.newBuilder()
.setRuleChainIdMSB(rootRuleChainId.getId().getMostSignificantBits())
.setRuleChainIdLSB(rootRuleChainId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(builder);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder()
.addRuleChainMetadataRequestMsg(builder.build());
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
}
private RuleChainId getEdgeRootRuleChainId() throws Exception {
return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference<PageData<RuleChain>>() {
},
new PageLink(100, 0, "Edge Root Rule Chain"),
"EDGE")
.getData().get(0).getId();
}
@After
public void teardownEdgeTest() {
try {
@ -213,6 +190,19 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class);
}
private RuleChainId getEdgeRootRuleChainId() throws Exception {
List<RuleChain> edgeRuleChains = doGetTypedWithPageLink("/api/ruleChains?type={type}&",
new TypeReference<PageData<RuleChain>>() {},
new PageLink(100, 0, "Edge Root Rule Chain"),
"EDGE").getData();
for (RuleChain edgeRuleChain : edgeRuleChains) {
if (edgeRuleChain.isRoot()) {
return edgeRuleChain.getId();
}
}
throw new RuntimeException("Root rule chain not found");
}
protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
DeviceProfileData profileData = deviceProfile.getProfileData();
List<DeviceProfileAlarm> alarms = new ArrayList<>();
@ -255,8 +245,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(RuleChainUpdateMsg.class, 1);
UUID ruleChainUUID = validateRuleChains();
// 1 from request message
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2);
// 1 from rule chain fetcher
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1);
validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages ('general', 'mail', 'connectivity', 'jwt')
@ -438,12 +428,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) {
Optional<RuleChainMetadataUpdateMsg> ruleChainMetadataUpdateOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class);
Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get();
Optional<RuleChainMetadataUpdateMsg> ruleChainMetadataUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class);
Assert.assertTrue(ruleChainMetadataUpdateMsgOpt.isPresent());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType());
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetaData);
Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId());
}

27
application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java

@ -169,6 +169,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
public void testSendAssetToCloud() throws Exception {
Asset asset = buildAssetForUplinkMsg("Asset Edge 2");
// created asset on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
assetUpdateMsgBuilder.setIdMSB(asset.getUuidId().getMostSignificantBits());
@ -191,6 +192,32 @@ public class AssetEdgeTest extends AbstractEdgeTest {
Asset foundAsset = doGet("/api/asset/" + asset.getUuidId(), Asset.class);
Assert.assertNotNull(foundAsset);
Assert.assertEquals("Asset Edge 2", foundAsset.getName());
// update asset on edge
asset.setName("Asset Edge 2 Updated");
uplinkMsgBuilder = UplinkMsg.newBuilder();
assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
assetUpdateMsgBuilder.setIdMSB(asset.getUuidId().getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(asset.getUuidId().getLeastSignificantBits());
assetUpdateMsgBuilder.setEntity(JacksonUtil.toString(asset));
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
foundAsset = doGet("/api/asset/" + asset.getUuidId(), Asset.class);
Assert.assertNotNull(foundAsset);
Assert.assertEquals("Asset Edge 2 Updated", foundAsset.getName());
}
@Test

1
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -184,6 +184,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
// create dashboard on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder();
dashboardUpdateMsgBuilder.setIdMSB(dashboard.getUuidId().getMostSignificantBits());

24
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -593,8 +593,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
@Test
public void testSendDeviceToCloud() throws Exception {
Device deviceMsg = buildDeviceForUplinkMsg("Edge Device 2", "test");
String deviceName = "Edge Device 2";
Device deviceMsg = buildDeviceForUplinkMsg(deviceName, "test");
// create device on edge
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits());
@ -609,7 +611,25 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
Device device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class);
Assert.assertNotNull(device);
Assert.assertEquals("Edge Device 2", device.getName());
Assert.assertEquals(deviceName, device.getName());
// update device on edge
deviceMsg.setName(deviceName + " Updated");
uplinkMsgBuilder = UplinkMsg.newBuilder();
deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceUpdateMsgBuilder.setIdMSB(deviceMsg.getUuidId().getMostSignificantBits());
deviceUpdateMsgBuilder.setIdLSB(deviceMsg.getUuidId().getLeastSignificantBits());
deviceUpdateMsgBuilder.setEntity(JacksonUtil.toString(deviceMsg));
deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class);
Assert.assertNotNull(device);
Assert.assertEquals(deviceName + " Updated", device.getName());
}
@Test

84
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.edge;
import com.google.protobuf.AbstractMessage;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
@ -29,16 +29,17 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -74,8 +75,9 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetaData);
Assert.assertEquals(ruleChainMetaData.getRuleChainId(), savedRuleChain.getId());
testRuleChainMetadataRequestMsg(savedRuleChain.getId());
for (RuleNode ruleNode : ruleChainMetaData.getNodes()) {
Assert.assertEquals(CONFIGURATION_VERSION, ruleNode.getConfigurationVersion());
}
// unassign rule chain from edge
edgeImitator.expectMessageAmount(1);
@ -97,60 +99,62 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
}
@Test
public void testSendRuleChainMetadataRequestToCloud() throws Exception {
RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId();
public void testRuleChainToCloud() throws Exception {
String ruleChainName = "Rule Chain Edge";
UUID uuid = Uuids.timeBased();
// create rule chain on edge
RuleChain edgeRuleChain = new RuleChain();
edgeRuleChain.setTenantId(tenantId);
edgeRuleChain.setId(new RuleChainId(uuid));
edgeRuleChain.setName(ruleChainName);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder();
ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits());
ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder);
uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build());
RuleChainUpdateMsg.Builder ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder();
ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
ruleChainUpdateMsgBuilder.setEntity(JacksonUtil.toString(edgeRuleChain));
ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder);
uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg);
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage;
RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetadataMsg);
Assert.assertEquals(edgeRootRuleChainId, ruleChainMetadataMsg.getRuleChainId());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg);
}
RuleChain ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class);
Assert.assertNotNull(ruleChain);
Assert.assertEquals("Rule Chain Edge", ruleChain.getName());
private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception {
RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder()
.setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits())
.setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder);
// update rule chain on edge
edgeRuleChain.setName(ruleChainName + " Updated");
uplinkMsgBuilder = UplinkMsg.newBuilder();
ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder();
ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
ruleChainUpdateMsgBuilder.setEntity(JacksonUtil.toString(edgeRuleChain));
ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder);
uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build());
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder()
.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg);
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage;
RuleChainMetaData ruleChainMetadataMsg = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetadataMsg);
Assert.assertEquals(ruleChainId, ruleChainMetadataMsg.getRuleChainId());
latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
for (RuleNode ruleNode : ruleChainMetadataMsg.getNodes()) {
Assert.assertEquals(CONFIGURATION_VERSION, ruleNode.getConfigurationVersion());
}
ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class);
Assert.assertNotNull(ruleChain);
Assert.assertEquals(ruleChainName + " Updated", ruleChain.getName());
}
private RuleChainMetaData createRuleChainMetadata(RuleChain ruleChain) {

144
application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java

@ -0,0 +1,144 @@
/**
* 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.apiusage;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.controller.TbUrlConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"usage.stats.report.enabled=true",
"usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1",
})
public class ApiUsageTest extends AbstractControllerTest {
private Tenant savedTenant;
private User tenantAdmin;
private static final int MAX_DP_ENABLE_VALUE = 12;
private static final double WARN_THRESHOLD_VALUE = 0.5;
@Autowired
private ApiUsageStateService apiUsageStateService;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = createTenantProfile();
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
tenant.setTenantProfileId(savedTenantProfile.getId());
savedTenant = saveTenant(tenant);
tenantId = savedTenant.getId();
assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
}
@Test
public void testTelemetryApiCall() throws Exception {
Device device = createDevice();
assertNotNull(device);
String telemetryPayload = "{\"temperature\":25, \"humidity\":60}";
String url = TbUrlConstants.TELEMETRY_URL_PREFIX + "/DEVICE/" + device.getId() + "/timeseries/ANY";
long VALUE_WARNING = (long) (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE) / 2;
for (int i = 0; i < VALUE_WARNING; i++) {
doPostAsync(url, telemetryPayload, String.class, status().isOk());
}
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()));
long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2;
for (int i = 0; i < VALUE_DISABLE; i++) {
doPostAsync(url, telemetryPayload, String.class, status().isOk());
}
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
});
}
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile");
tenantProfile.setDescription("Tenant Profile" + " Test");
TenantProfileData tenantProfileData = new TenantProfileData();
DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder()
.maxDPStorageDays(MAX_DP_ENABLE_VALUE)
.warnThreshold(WARN_THRESHOLD_VALUE)
.build();
tenantProfileData.setConfiguration(config);
tenantProfile.setProfileData(tenantProfileData);
return tenantProfile;
}
private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN";
Device device = new Device();
device.setName("My device");
device.setType("default");
device.setTenantId(tenantId);
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(testToken);
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials);
return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class);
}
}

123
application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java

@ -15,45 +15,114 @@
*/
package org.thingsboard.server.service.apiusage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.ApiUsageState;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.never;
import static org.junit.Assert.assertEquals;
@ExtendWith(MockitoExtension.class)
public class DefaultTbApiUsageStateServiceTest {
@DaoSqlTest
public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
@Mock
TenantApiUsageState tenantUsageStateMock;
@Autowired
DefaultTbApiUsageStateService service;
TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"));
@Autowired
private ApiUsageStateService apiUsageStateService;
@Spy
@InjectMocks
DefaultTbApiUsageStateService service;
private TenantId tenantId;
private Tenant savedTenant;
private static final int MAX_ENABLE_VALUE = 5000;
private static final long VALUE_WARNING = 4500L;
private static final long VALUE_DISABLE = 5500L;
private static final double WARN_THRESHOLD_VALUE = 0.8;
@Before
public void init() throws Exception {
loginSysAdmin();
@BeforeEach
public void setUp() {
TenantProfile tenantProfile = createTenantProfile();
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
tenant.setTenantProfileId(savedTenantProfile.getId());
savedTenant = saveTenant(tenant);
tenantId = savedTenant.getId();
Assert.assertNotNull(savedTenant);
}
@Test
public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() {
service.myUsageStates.put(tenantId, tenantUsageStateMock);
ApiUsageState tenantUsageState = service.getApiUsageState(tenantId);
assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState()));
Mockito.verify(service, never()).getOrFetchState(tenantId, tenantId);
public void testProcess_transitionFromWarningToDisabled() {
TransportProtos.ToUsageStatsServiceMsg.Builder warningMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setCustomerIdMSB(0)
.setCustomerIdLSB(0)
.setServiceId("testService");
warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder()
.setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name())
.setValue(VALUE_WARNING)
.build());
TransportProtos.ToUsageStatsServiceMsg warningStatsMsg = warningMsgBuilder.build();
TbProtoQueueMsg<TransportProtos.ToUsageStatsServiceMsg> warningMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), warningStatsMsg);
service.process(warningMsg, TbCallback.EMPTY);
assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
TransportProtos.ToUsageStatsServiceMsg.Builder disableMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setCustomerIdMSB(0)
.setCustomerIdLSB(0)
.setServiceId("testService");
disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder()
.setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name())
.setValue(VALUE_DISABLE)
.build());
TransportProtos.ToUsageStatsServiceMsg disableStatsMsg = disableMsgBuilder.build();
TbProtoQueueMsg<TransportProtos.ToUsageStatsServiceMsg> disableMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), disableStatsMsg);
service.process(disableMsg, TbCallback.EMPTY);
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
}
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile");
tenantProfile.setDescription("Tenant Profile" + " Test");
TenantProfileData tenantProfileData = new TenantProfileData();
DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder()
.maxDPStorageDays(MAX_ENABLE_VALUE)
.warnThreshold(WARN_THRESHOLD_VALUE)
.build();
tenantProfileData.setConfiguration(config);
tenantProfile.setProfileData(tenantProfileData);
return tenantProfile;
}
}

221
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

File diff suppressed because one or more lines are too long

5
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -69,9 +70,7 @@ public interface ResourceService extends EntityDaoService {
PageData<TbResource> findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink);
void deleteResource(TenantId tenantId, TbResourceId resourceId);
void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force);
TbResourceDeleteResult deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force);
void deleteResourcesByTenantId(TenantId tenantId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java

@ -46,6 +46,8 @@ public interface RuleChainService extends EntityDaoService {
RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent);
RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate);
boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId);
RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function<RuleNode, RuleNode> ruleNodeUpdater);

1
common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java

@ -34,4 +34,5 @@ public interface ApiUsageStateService extends EntityDaoService {
void deleteApiUsageStateByEntityId(EntityId entityId);
ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id);
}

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

@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenantId {
public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenantId, HasVersion {
private static final long serialVersionUID = 8250339805336035966L;
@ -41,6 +41,7 @@ public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenan
private ApiUsageStateValue emailExecState;
private ApiUsageStateValue smsExecState;
private ApiUsageStateValue alarmExecState;
private Long version;
public ApiUsageState() {
super();
@ -62,6 +63,7 @@ public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenan
this.emailExecState = ur.getEmailExecState();
this.smsExecState = ur.getSmsExecState();
this.alarmExecState = ur.getAlarmExecState();
this.version = ur.getVersion();
}
public boolean isTransportEnabled() {
@ -84,15 +86,16 @@ public class ApiUsageState extends BaseData<ApiUsageStateId> implements HasTenan
return !ApiUsageStateValue.DISABLED.equals(tbelExecState);
}
public boolean isEmailSendEnabled(){
public boolean isEmailSendEnabled() {
return !ApiUsageStateValue.DISABLED.equals(emailExecState);
}
public boolean isSmsSendEnabled(){
public boolean isSmsSendEnabled() {
return !ApiUsageStateValue.DISABLED.equals(smsExecState);
}
public boolean isAlarmCreationEnabled() {
return alarmExecState != ApiUsageStateValue.DISABLED;
}
}

32
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java

@ -0,0 +1,32 @@
/**
* 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.common.data;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.HasId;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class TbResourceDeleteResult {
private boolean success;
private Map<String, List<? extends HasId<?>>> references;
}

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

@ -42,6 +42,8 @@ enum EdgeVersion {
V_3_8_0 = 8;
V_3_9_0 = 9;
V_4_0_0 = 10;
V_LATEST = 999;
}
/**
@ -303,7 +305,9 @@ message NotificationTemplateUpdateMsg {
optional string entity = 4;
}
// DEPRECATED. FOR REMOVAL
message RuleChainMetadataRequestMsg {
option deprecated = true;
int64 ruleChainIdMSB = 1;
int64 ruleChainIdLSB = 2;
}
@ -321,22 +325,30 @@ message RelationRequestMsg {
string entityType = 3;
}
// DEPRECATED. FOR REMOVAL
message UserCredentialsRequestMsg {
option deprecated = true;
int64 userIdMSB = 1;
int64 userIdLSB = 2;
}
// DEPRECATED. FOR REMOVAL
message DeviceCredentialsRequestMsg {
option deprecated = true;
int64 deviceIdMSB = 1;
int64 deviceIdLSB = 2;
}
// DEPRECATED. FOR REMOVAL
message WidgetBundleTypesRequestMsg {
option deprecated = true;
int64 widgetBundleIdMSB = 1;
int64 widgetBundleIdLSB = 2;
}
// DEPRECATED. FOR REMOVAL
message EntityViewsRequestMsg {
option deprecated = true;
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
string entityType = 3;
@ -394,14 +406,14 @@ message UplinkMsg {
repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4;
repeated AlarmUpdateMsg alarmUpdateMsg = 5;
repeated RelationUpdateMsg relationUpdateMsg = 6;
repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7;
repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7 [deprecated = true];
repeated AttributesRequestMsg attributesRequestMsg = 8;
repeated RelationRequestMsg relationRequestMsg = 9;
repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10;
repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11;
repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10 [deprecated = true];
repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11 [deprecated = true];
repeated DeviceRpcCallMsg deviceRpcCallMsg = 12;
repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14;
repeated EntityViewsRequestMsg entityViewsRequestMsg = 15;
repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14 [deprecated = true];
repeated EntityViewsRequestMsg entityViewsRequestMsg = 15 [deprecated = true];
repeated AssetUpdateMsg assetUpdateMsg = 16;
repeated DashboardUpdateMsg dashboardUpdateMsg = 17;
repeated EntityViewUpdateMsg entityViewUpdateMsg = 18;
@ -409,6 +421,8 @@ message UplinkMsg {
repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20;
repeated ResourceUpdateMsg resourceUpdateMsg = 21;
repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22;
repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23;
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
}
message UplinkResponseMsg {
@ -427,7 +441,7 @@ message DownlinkMsg {
int32 downlinkMsgId = 1;
SyncCompletedMsg syncCompletedMsg = 2;
repeated EntityDataProto entityData = 3;
repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4;
repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4 [deprecated = true];
repeated DeviceUpdateMsg deviceUpdateMsg = 5;
repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 6;
repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 7;

5
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -1143,7 +1143,9 @@ public class ProtoUtils {
.setTbelExecState(apiUsageState.getTbelExecState().name())
.setEmailExecState(apiUsageState.getEmailExecState().name())
.setSmsExecState(apiUsageState.getSmsExecState().name())
.setAlarmExecState(apiUsageState.getAlarmExecState().name()).build();
.setAlarmExecState(apiUsageState.getAlarmExecState().name())
.setVersion(apiUsageState.getVersion())
.build();
}
public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) {
@ -1159,6 +1161,7 @@ public class ProtoUtils {
apiUsageState.setEmailExecState(ApiUsageStateValue.valueOf(proto.getEmailExecState()));
apiUsageState.setSmsExecState(ApiUsageStateValue.valueOf(proto.getSmsExecState()));
apiUsageState.setAlarmExecState(ApiUsageStateValue.valueOf(proto.getAlarmExecState()));
apiUsageState.setVersion(proto.getVersion());
return apiUsageState;
}

1
common/proto/src/main/proto/queue.proto

@ -327,6 +327,7 @@ message ApiUsageStateProto {
string emailExecState = 14;
string smsExecState = 15;
string alarmExecState = 16;
int64 version = 17;
}
message RepositorySettingsProto {

56
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java

@ -32,6 +32,24 @@ public class TopicService {
@Value("${queue.prefix:}")
private String prefix;
@Value("${queue.core.notifications-topic:tb_core.notifications}")
private String tbCoreNotificationsTopic;
@Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}")
private String tbRuleEngineNotificationsTopic;
@Value("${queue.transport.notifications-topics:tb_transport.notifications}")
private String tbTransportNotificationsTopic;
@Value("${queue.edge.notifications-topic:tb_edge.notifications}")
private String tbEdgeNotificationsTopic;
@Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}")
private String tbEdgeEventNotificationsTopic;
@Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}")
private String tbCalculatedFieldNotificationsTopic;
private final ConcurrentMap<String, TopicPartitionInfo> tbCoreNotificationTopics = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TopicPartitionInfo> tbRuleEngineNotificationTopics = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TopicPartitionInfo> tbEdgeNotificationTopics = new ConcurrentHashMap<>();
@ -48,24 +66,32 @@ public class TopicService {
public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) {
return switch (serviceType) {
case TB_CORE -> tbCoreNotificationTopics.computeIfAbsent(serviceId,
id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId));
id -> buildNotificationsTopicPartitionInfo(tbCoreNotificationsTopic, serviceId));
case TB_RULE_ENGINE -> tbRuleEngineNotificationTopics.computeIfAbsent(serviceId,
id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId));
default -> buildNotificationsTopicPartitionInfo(serviceType, serviceId);
id -> buildNotificationsTopicPartitionInfo(tbRuleEngineNotificationsTopic, serviceId));
case TB_TRANSPORT -> buildNotificationsTopicPartitionInfo(tbTransportNotificationsTopic, serviceId);
default -> throw new IllegalStateException("Unexpected service type: " + serviceType);
};
}
private TopicPartitionInfo buildNotificationsTopicPartitionInfo(String topic, String serviceId) {
return buildTopicPartitionInfo(buildNotificationTopicName(topic, serviceId), null, null, false);
}
public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) {
return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition);
}
public TopicPartitionInfo getEdgeNotificationsTopic(String serviceId) {
return tbEdgeNotificationTopics.computeIfAbsent(serviceId, id -> buildEdgeNotificationsTopicPartitionInfo(serviceId));
}
private TopicPartitionInfo buildEdgeNotificationsTopicPartitionInfo(String serviceId) {
return buildTopicPartitionInfo("tb_edge.notifications." + serviceId, null, null, false);
return buildTopicPartitionInfo(buildNotificationTopicName(tbEdgeNotificationsTopic, serviceId), null, null, false);
}
public TopicPartitionInfo getCalculatedFieldNotificationsTopic(String serviceId) {
return tbCalculatedFieldNotificationTopics.computeIfAbsent(serviceId,
id -> buildNotificationsTopicPartitionInfo("calculated_field", serviceId));
return tbCalculatedFieldNotificationTopics.computeIfAbsent(serviceId, id -> buildNotificationsTopicPartitionInfo(tbCalculatedFieldNotificationsTopic, serviceId));
}
public TopicPartitionInfo getEdgeEventNotificationsTopic(TenantId tenantId, EdgeId edgeId) {
@ -73,25 +99,17 @@ public class TopicService {
}
public TopicPartitionInfo buildEdgeEventNotificationsTopicPartitionInfo(TenantId tenantId, EdgeId edgeId) {
return buildTopicPartitionInfo("tb_edge_event.notifications." + tenantId + "." + edgeId, null, null, false);
}
private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) {
return buildNotificationsTopicPartitionInfo(serviceType.name().toLowerCase(), serviceId);
}
private TopicPartitionInfo buildNotificationsTopicPartitionInfo(String serviceType, String serviceId) {
return buildTopicPartitionInfo(serviceType + ".notifications." + serviceId, null, null, false);
}
public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) {
return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition);
return buildTopicPartitionInfo(tbEdgeEventNotificationsTopic + "." + tenantId + "." + edgeId, null, null, false);
}
public String buildTopicName(String topic) {
return prefix.isBlank() ? topic : prefix + "." + topic;
}
private String buildNotificationTopicName(String topic, String serviceId) {
return topic + "." + serviceId;
}
public String buildConsumerGroupId(String servicePrefix, TenantId tenantId, String queueName, Integer partitionId) {
return this.buildTopicName(
servicePrefix + queueName

38
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -37,6 +37,7 @@ import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Function;
public class TbDate implements Serializable, Cloneable {
@ -481,6 +482,43 @@ public class TbDate implements Serializable, Cloneable {
instant = Instant.ofEpochMilli(dateMilliSecond);
}
public void addDays(int days) {
adjustTime(zonedDateTime -> zonedDateTime.plusDays(days));
}
public void addYears(int years) {
adjustTime(zonedDateTime -> zonedDateTime.plusYears(years));
}
public void addMonths(int months) {
adjustTime(zonedDateTime -> zonedDateTime.plusMonths(months));
}
public void addWeeks(int weeks) {
adjustTime(zonedDateTime -> zonedDateTime.plusWeeks(weeks));
}
public void addHours(int hours) {
adjustTime(zonedDateTime -> zonedDateTime.plusHours(hours));
}
public void addMinutes(int minutes) {
adjustTime(zonedDateTime -> zonedDateTime.plusMinutes(minutes));
}
public void addSeconds(int seconds) {
adjustTime(zonedDateTime -> zonedDateTime.plusSeconds(seconds));
}
public void addNanos(long nanos) {
adjustTime(zonedDateTime -> zonedDateTime.plusNanos(nanos));
}
private void adjustTime(Function<ZonedDateTime, ZonedDateTime> adjuster) {
ZonedDateTime zonedDateTime = adjuster.apply(getZonedDateTime());
this.instant = zonedDateTime.toInstant();
}
public ZoneOffset getLocaleZoneOffset(Instant... instants){
return ZoneId.systemDefault().getRules().getOffset(instants.length > 0 ? instants[0] : this.instant);
}

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -1302,7 +1302,7 @@ public class TbUtils {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("[+-]?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
return str.matches("[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+)?") ? DEC_RADIX : -1;
}
public static int isHexadecimal(String str) {

60
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java

@ -885,4 +885,64 @@ class TbDateTest {
Assertions.assertNotNull(serializedTbDate);
Assertions.assertEquals(expectedDate.toString(), serializedTbDate);
}
@Test
void testAddFunctions() {
TbDate d = new TbDate(2024, 1, 1, 10, 0, 0, 0);
testResultChangeDateTime(d);
d.addYears(1);
testResultChangeDateTime(d);
d.addYears(-2);
testResultChangeDateTime(d);
d.addMonths(2);
testResultChangeDateTime(d);
d.addMonths(10);
testResultChangeDateTime(d);
d.addMonths(-13);
testResultChangeDateTime(d);
d.addWeeks(4);
testResultChangeDateTime(d);
d.addWeeks(-5);
testResultChangeDateTime(d);
d.addDays(6);
testResultChangeDateTime(d);
d.addDays(45);
testResultChangeDateTime(d);
d.addDays(-50);
testResultChangeDateTime(d);
d.addHours(23);
testResultChangeDateTime(d);
d.addHours(-47);
testResultChangeDateTime(d);
d.addMinutes(59);
testResultChangeDateTime(d);
d.addMinutes(-60);
testResultChangeDateTime(d);
d.addSeconds(59);
testResultChangeDateTime(d);
d.addSeconds(-60);
testResultChangeDateTime(d);
d.addNanos(999999);
testResultChangeDateTime(d);
d.addNanos(-1000000);
testResultChangeDateTime(d);
}
}

11
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -442,8 +442,9 @@ public class TbUtilsTest {
@Test
public void parsDouble() {
String doubleValStr = "1729.1729";
Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr));
String doubleValStr = "1.1428250947E8";
Assertions.assertEquals(Double.parseDouble(doubleValStr), TbUtils.parseDouble(doubleValStr));
doubleValStr = "1729.1729";
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
@ -930,7 +931,13 @@ public class TbUtilsTest {
@Test
public void isDecimal_Test() {
Assertions.assertEquals(10, TbUtils.isDecimal("4567039"));
Assertions.assertEquals(10, TbUtils.isDecimal("1.1428250947E8"));
Assertions.assertEquals(10, TbUtils.isDecimal("123.45"));
Assertions.assertEquals(10, TbUtils.isDecimal("-1.23E-4"));
Assertions.assertEquals(10, TbUtils.isDecimal("1E5"));
Assertions.assertEquals(-1, TbUtils.isDecimal("C100110"));
Assertions.assertEquals(-1, TbUtils.isDecimal("abc"));
Assertions.assertEquals(-1, TbUtils.isDecimal(null));
}
@Test

29
dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java

@ -0,0 +1,29 @@
/**
* 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.dao;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.List;
public interface ResourceContainerDao<T extends HasId<?>> {
List<T> findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit);
List<T> findByResourceLink(String link, int limit);
}

3
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java

@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.ResourceContainerDao;
import java.util.UUID;
/**
* The Interface DashboardInfoDao.
*/
public interface DashboardInfoDao extends Dao<DashboardInfo>, ImageContainerDao<DashboardInfo> {
public interface DashboardInfoDao extends Dao<DashboardInfo>, ImageContainerDao<DashboardInfo>, ResourceContainerDao<DashboardInfo> {
/**
* Find dashboards by tenantId and page link.

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

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.BaseVersionedEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.UUID;
@ -40,7 +40,7 @@ import java.util.UUID;
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = ModelConstants.API_USAGE_STATE_TABLE_NAME)
public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements BaseEntity<ApiUsageState> {
public class ApiUsageStateEntity extends BaseVersionedEntity<ApiUsageState> implements BaseEntity<ApiUsageState> {
@Column(name = ModelConstants.API_USAGE_STATE_TENANT_ID_COLUMN)
private UUID tenantId;
@ -77,10 +77,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements
}
public ApiUsageStateEntity(ApiUsageState ur) {
if (ur.getId() != null) {
this.setUuid(ur.getId().getId());
}
this.setCreatedTime(ur.getCreatedTime());
super(ur);
if (ur.getTenantId() != null) {
this.tenantId = ur.getTenantId().getId();
}
@ -116,6 +113,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity<ApiUsageState> implements
ur.setEmailExecState(emailExecState);
ur.setSmsExecState(smsExecState);
ur.setAlarmExecState(alarmExecState);
ur.setVersion(version);
return ur;
}

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java

@ -15,9 +15,7 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
@ -61,8 +59,7 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
}
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) {
this.setUuid(calculatedFieldLink.getUuidId());
this.createdTime = calculatedFieldLink.getCreatedTime();
super(calculatedFieldLink);
this.tenantId = calculatedFieldLink.getTenantId().getId();
this.entityType = calculatedFieldLink.getEntityId().getEntityType().name();
this.entityId = calculatedFieldLink.getEntityId().getId();

12
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -75,8 +75,6 @@ import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
@Slf4j
public class BaseImageService extends BaseResourceService implements ImageService {
private static final int MAX_ENTITIES_TO_FIND = 10;
public static Map<String, String> DASHBOARD_BASE64_MAPPING = new HashMap<>();
public static Map<String, String> WIDGET_TYPE_BASE64_MAPPING = new HashMap<>();
@ -107,19 +105,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic
private final AssetProfileDao assetProfileDao;
private final DeviceProfileDao deviceProfileDao;
private final WidgetsBundleDao widgetsBundleDao;
private final WidgetTypeDao widgetTypeDao;
private final DashboardInfoDao dashboardInfoDao;
private final Map<EntityType, ImageContainerDao<?>> imageContainerDaoMap = new HashMap<>();
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator,
AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao,
WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) {
super(resourceDao, resourceInfoDao, resourceValidator);
super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao);
this.assetProfileDao = assetProfileDao;
this.deviceProfileDao = deviceProfileDao;
this.widgetsBundleDao = widgetsBundleDao;
this.widgetTypeDao = widgetTypeDao;
this.dashboardInfoDao = dashboardInfoDao;
}
@PostConstruct
@ -131,7 +125,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic
imageContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
}
@Override
@SneakyThrows
public TbResourceInfo saveImage(TbResource image) {
@ -311,7 +304,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
}
if (success) {
deleteResource(tenantId, imageId, force);
success = deleteResource(tenantId, imageId, true)
.isSuccess();
}
return result.success(success).build();
}

60
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.EntityId;
@ -48,6 +50,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.ResourceContainerDao;
import org.thingsboard.server.dao.dashboard.DashboardInfoDao;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@ -55,6 +59,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -85,6 +90,17 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
protected final WidgetTypeDao widgetTypeDao;
protected final DashboardInfoDao dashboardInfoDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct
public void init() {
resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
}
@Autowired @Lazy
private ImageService imageService;
@ -313,23 +329,45 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
}
@Override
public void deleteResource(TenantId tenantId, TbResourceId resourceId) {
deleteResource(tenantId, resourceId, false);
}
@Override
public void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force) {
public TbResourceDeleteResult deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, id -> INCORRECT_RESOURCE_ID + id);
TbResourceInfo resource = findResourceInfoById(tenantId, resourceId);
boolean success = true;
var result = TbResourceDeleteResult.builder();
if (resource == null) {
return;
if (!force) {
success = false;
}
return result.success(success).build();
}
if (!force) {
resourceValidator.validateDelete(tenantId, resource);
if (resource.getResourceType() == ResourceType.JS_MODULE) {
var link = resource.getLink();
Map<String, List<? extends HasId<?>>> affectedEntities = new HashMap<>();
resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> {
var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) :
resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND);
if (!entities.isEmpty()) {
affectedEntities.put(entityType.name(), entities);
}
});
if (!affectedEntities.isEmpty()) {
success = false;
result.references(affectedEntities);
}
}
}
resourceDao.removeById(tenantId, resourceId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build());
if (success) {
resourceDao.removeById(tenantId, resourceId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build());
}
return result.success(success).build();
}
@Override
@ -666,7 +704,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
protected void removeEntity(TenantId tenantId, TbResourceId resourceId) {
deleteResource(tenantId, resourceId);
deleteResource(tenantId, resourceId, true);
}
};

11
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -118,7 +118,16 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
@Override
@Transactional
public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent) {
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
return saveRuleChain(ruleChain, publishSaveEvent, true);
}
@Override
@Transactional
public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) {
log.trace("Executing doSaveRuleChain [{}]", ruleChain);
if (doValidate) {
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
}
try {
RuleChain savedRuleChain = ruleChainDao.saveAndFlush(ruleChain.getTenantId(), ruleChain);
if (ruleChain.getId() == null) {

15
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -30,9 +29,6 @@ import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.List;
import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
@ -42,9 +38,6 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Autowired
private TbResourceDao resourceDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Autowired
private TenantService tenantService;
@ -111,12 +104,4 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
}
public void validateDelete(TenantId tenantId, TbResourceInfo resourceInfo) {
List<String> widgets = widgetTypeDao.findWidgetTypesNamesByTenantIdAndResourceLink(tenantId.getId(), resourceInfo.getLink());
if (!widgets.isEmpty()) {
throw new DataValidationException("Following widget types use this resource: " + String.join(", ", widgets));
}
}
}

20
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -88,10 +88,10 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
boolean flushed = false;
EntityManager entityManager = getEntityManager();
if (isNew) {
entityManager.persist(entity);
if (entity instanceof HasVersion versionedEntity) {
versionedEntity.setVersion(1L);
}
entityManager.persist(entity);
} else {
if (entity instanceof HasVersion versionedEntity) {
if (versionedEntity.getVersion() == null) {
@ -106,23 +106,25 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
}
}
versionedEntity = entityManager.merge(versionedEntity);
entity = (E) versionedEntity;
/*
* by default, Hibernate doesn't issue an update query and thus version increment
* if the entity was not modified. to bypass this and always increment the version, we do it manually
* */
versionedEntity.setVersion(versionedEntity.getVersion() + 1);
/*
* flushing and then removing the entity from the persistence context so that it is not affected
* by next flushes (e.g. when a transaction is committed) to avoid double version increment
* */
entityManager.flush();
entityManager.detach(versionedEntity);
flushed = true;
entity = (E) versionedEntity;
} else {
entity = entityManager.merge(entity);
}
}
if (entity instanceof HasVersion versionedEntity) {
/*
* flushing and then removing the entity from the persistence context so that it is not affected
* by next flushes (e.g. when a transaction is committed) to avoid double version increment
* */
entityManager.flush();
entityManager.detach(versionedEntity);
flushed = true;
}
if (flush && !flushed) {
entityManager.flush();
}

16
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java

@ -78,13 +78,21 @@ public interface DashboardInfoRepository extends JpaRepository<DashboardInfoEnti
@Query(nativeQuery = true,
value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId " +
"and (d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :lmt"
"and (d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :limit"
)
List<DashboardInfoEntity> findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt);
List<DashboardInfoEntity> findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit);
@Query(nativeQuery = true,
value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt"
value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :limit"
)
List<DashboardInfoEntity> findByImageLink(@Param("imageLink") String imageLink, @Param("lmt") int lmt);
List<DashboardInfoEntity> findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit);
@Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
nativeQuery = true)
List<DashboardInfoEntity> findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit);
@Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
nativeQuery = true)
List<DashboardInfoEntity> findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit);
}

11
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java

@ -133,4 +133,15 @@ public class JpaDashboardInfoDao extends JpaAbstractDao<DashboardInfoEntity, Das
public List<DashboardInfo> findByImageLink(String imageLink, int limit) {
return DaoUtil.convertDataList(dashboardInfoRepository.findByImageLink(imageLink, limit));
}
@Override
public List<DashboardInfo> findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) {
return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit));
}
@Override
public List<DashboardInfo> findByResourceLink(String link, int limit) {
return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit));
}
}

15
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -190,11 +190,6 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndFqn(tenantId, fqn));
}
@Override
public List<String> findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link) {
return widgetTypeRepository.findNamesByTenantIdAndResourceLink(tenantId, link);
}
@Override
public List<WidgetTypeId> findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List<String> widgetFqns) {
var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns);
@ -266,4 +261,14 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
}
@Override
public List<WidgetTypeInfo> findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) {
return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit));
}
@Override
public List<WidgetTypeInfo> findByResourceLink(String link, int limit) {
return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit));
}
}

15
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java

@ -204,13 +204,20 @@ public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEn
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " +
"(select id from widget_type where tenant_id = :tenantId " +
"and (image = :imageLink or descriptor ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :lmt)"
"and (image = :imageLink or descriptor ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :limit)"
)
List<WidgetTypeInfoEntity> findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt);
List<WidgetTypeInfoEntity> findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " +
"(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :lmt)"
"(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :limit)"
)
List<WidgetTypeInfoEntity> findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt);
List<WidgetTypeInfoEntity> findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit);
@Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true)
List<WidgetTypeInfoEntity> findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit);
@Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true)
List<WidgetTypeInfoEntity> findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit);
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -69,12 +69,6 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
WidgetTypeDetailsEntity findByTenantIdAndFqn(UUID tenantId, String fqn);
@Query(value = "SELECT name FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE concat('%', :resourceLink, '%')",
nativeQuery = true)
List<String> findNamesByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId,
@Param("resourceLink") String resourceLink);
@Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);

1
dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java

@ -154,6 +154,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
log.trace("Executing save [{}]", apiUsageState.getTenantId());
validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id);
validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!");
apiUsageState.setVersion(null);
ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId())
.entity(savedState).build());

5
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.ResourceContainerDao;
import java.util.List;
import java.util.UUID;
@ -35,7 +36,7 @@ import java.util.UUID;
/**
* The Interface WidgetTypeDao.
*/
public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityDao<WidgetTypeId, WidgetTypeDetails>, ImageContainerDao<WidgetTypeInfo> {
public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityDao<WidgetTypeId, WidgetTypeDetails>, ImageContainerDao<WidgetTypeInfo>, ResourceContainerDao<WidgetTypeInfo> {
/**
* Save or update widget type object
@ -97,8 +98,6 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityD
WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn);
List<String> findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link);
List<WidgetTypeId> findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List<String> widgetFqns);
List<WidgetsBundleWidget> findWidgetsBundleWidgetsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId);

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

@ -718,6 +718,7 @@ CREATE TABLE IF NOT EXISTS api_usage_state (
email_exec varchar(32),
sms_exec varchar(32),
alarm_exec varchar(32),
version BIGINT DEFAULT 1,
CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)
);

47
dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java

@ -20,6 +20,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
@ -30,22 +31,42 @@ public class ApiUsageStateServiceTest extends AbstractServiceTest {
ApiUsageStateService apiUsageStateService;
@Test
public void testFindApiUsageStateByTenantId() {
ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNotNull(apiUsageState);
public void testFindTenantApiUsageState() {
ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNotNull(state);
}
@Test
public void testUpdateApiUsageState(){
ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNotNull(apiUsageState);
Assert.assertTrue(apiUsageState.isTransportEnabled());
apiUsageState.setTransportState(ApiUsageStateValue.DISABLED);
apiUsageState = apiUsageStateService.update(apiUsageState);
Assert.assertNotNull(apiUsageState);
apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNotNull(apiUsageState);
Assert.assertFalse(apiUsageState.isTransportEnabled());
public void testUpdate() {
ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId);
state.setTransportState(ApiUsageStateValue.DISABLED);
ApiUsageState updated = apiUsageStateService.update(state);
Assert.assertEquals(ApiUsageStateValue.DISABLED, updated.getTransportState());
}
@Test
public void testUpdateWithNullId() {
ApiUsageState newState = new ApiUsageState();
newState.setTenantId(tenantId);
newState.setTransportState(ApiUsageStateValue.ENABLED);
Assert.assertThrows(IncorrectParameterException.class, () -> apiUsageStateService.update(newState));
}
@Test
public void testFindApiUsageStateByEntityId() {
ApiUsageState state = apiUsageStateService.findApiUsageStateByEntityId(tenantId);
Assert.assertNotNull(state);
}
@Test
public void testDeleteByTenantId() {
ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNotNull(state);
apiUsageStateService.deleteByTenantId(tenantId);
state = apiUsageStateService.findTenantApiUsageState(tenantId);
Assert.assertNull(state);
}
}

2
monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java

@ -145,7 +145,7 @@ public abstract class TransportHealthChecker<C extends TransportMonitoringConfig
profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration());
deviceProfile.setProfileData(profileData);
} else {
tbClient.getResources(new PageLink(1, 0, "LwM2M Monitoring id=3 v1.0")).getData()
tbClient.getResources(new PageLink(1, 0, "LwM2M Monitoring")).getData()
.stream().findFirst()
.orElseGet(() -> {
TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class);

6
monitoring/src/main/resources/lwm2m/device_profile.json

@ -12,14 +12,14 @@
"transportConfiguration": {
"observeAttr": {
"observe": [
"/3_1.0/0/0"
"/3_1.1/0/0"
],
"attribute": [],
"telemetry": [
"/3_1.0/0/0"
"/3_1.1/0/0"
],
"keyName": {
"/3_1.0/0/0": "testData"
"/3_1.1/0/0": "testData"
},
"attributeLwm2m": {}
},

324
monitoring/src/main/resources/lwm2m/models/test-model.xml

@ -1,45 +1,331 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
FILE INFORMATION
OMA Permanent Document
File: OMA-SUP-XML_3-V1_2-20201110-A.xml
Path: http://www.openmobilealliance.org/release/ObjLwM2M_Device/
OMNA LwM2M Registry
Path: https://github.com/OpenMobileAlliance/lwm2m-registry
Name: 3.xml
NORMATIVE INFORMATION
Information about this file can be found in the latest revision of
OMA-TS-LightweightM2M_Core-V1_2
This is available at http://www.openmobilealliance.org/release/LightweightM2M/
Send comments to https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues
LEGAL DISCLAIMER
Copyright © 2016-2025 The Thingsboard Authors
Copyright 2020 Open Mobile Alliance.
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
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
http://www.apache.org/licenses/LICENSE-2.0
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
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.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
The above license is used as a license under copyright only. Please
reference the OMA IPR Policy for patent licensing terms:
https://www.omaspecworks.org/about/intellectual-property-rights/
-->
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.openmobilealliance.org/tech/profiles/LWM2M-v1_1.xsd">
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.openmobilealliance.org/tech/profiles/LWM2M-v1_1.xsd">
<Object ObjectType="MODefinition">
<Name>LwM2M Monitoring</Name>
<Description1>
<![CDATA[]]></Description1>
<Description1><![CDATA[This LwM2M Object provides a range of device related information which can be queried by the LwM2M Server, and a device reboot and factory reset function.]]></Description1>
<ObjectID>3</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:3</ObjectURN>
<ObjectURN>urn:oma:lwm2m:oma:3:1.1</ObjectURN>
<LWM2MVersion>1.1</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<ObjectVersion>1.1</ObjectVersion>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Resources>
<Item ID="0">
<Name>Test data</Name>
<Name>Manufacturer</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Human readable manufacturer name]]></Description>
</Item>
<Item ID="1">
<Name>Model Number</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Test data]]></Description>
<Description><![CDATA[A model identifier (manufacturer specified string)]]></Description>
</Item>
</Resources>
<Item ID="2">
<Name>Serial Number</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Serial Number]]></Description>
</Item>
<Item ID="3">
<Name>Firmware Version</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Current firmware version of the Device.The Firmware Management function could rely on this resource.]]></Description>
</Item>
<Item ID="4">
<Name>Reboot</Name>
<Operations>E</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type></Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Reboot the LwM2M Device to restore the Device from unexpected firmware failure.]]></Description>
</Item>
<Item ID="5">
<Name>Factory Reset</Name>
<Operations>E</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type></Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Perform factory reset of the LwM2M Device to make the LwM2M Device to go through initial deployment sequence where provisioning and bootstrap sequence is performed. This requires client ensuring post factory reset to have minimal information to allow it to carry out one of the bootstrap methods specified in section 5.2.3.
When this Resource is executed, "De-register" operation MAY be sent to the LwM2M Server(s) before factory reset of the LwM2M Device.]]></Description>
</Item>
<Item ID="6">
<Name>Available Power Sources</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>0..7</RangeEnumeration>
<Units></Units>
<Description><![CDATA[0: DC power
1: Internal Battery
2: External Battery
3: Fuel Cell
4: Power over Ethernet
5: USB
6: AC (Mains) power
7: Solar
The same Resource Instance ID MUST be used to associate a given Power Source (Resource ID:6) with its Present Voltage (Resource ID:7) and its Present Current (Resource ID:8)]]></Description>
</Item>
<Item ID="7">
<Name>Power Source Voltage</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Present voltage for each Available Power Sources Resource Instance. The unit used for this resource is in mV.]]></Description>
</Item>
<Item ID="8">
<Name>Power Source Current</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Present current for each Available Power Source. The unit used for this resource is in mA.]]></Description>
</Item>
<Item ID="9">
<Name>Battery Level</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>0..100</RangeEnumeration>
<Units>/100</Units>
<Description><![CDATA[Contains the current battery level as a percentage (with a range from 0 to 100). This value is only valid for the Device internal Battery if present (one Available Power Sources Resource Instance is 1).]]></Description>
</Item>
<Item ID="10">
<Name>Memory Free</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Estimated current available amount of storage space which can store data and software in the LwM2M Device (expressed in kilobytes). Note: 1 kilobyte corresponds to 1000 bytes.]]></Description>
</Item>
<Item ID="11">
<Name>Error Code</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>0..32</RangeEnumeration>
<Units></Units>
<Description><![CDATA[0=No error
1=Low battery power
2=External power supply off
3=GPS module failure
4=Low received signal strength
5=Out of memory
6=SMS failure
7=IP connectivity failure
8=Peripheral malfunction
9..15=Reserved for future use
16..32=Device specific error codes
When the single Device Object Instance is initiated, there is only one error code Resource Instance whose value is equal to 0 that means no error. When the first error happens, the LwM2M Client changes error code Resource Instance to any non-zero value to indicate the error type. When any other error happens, a new error code Resource Instance is created. When an error associated with a Resource Instance is no longer present, that Resource Instance is deleted. When the single existing error is no longer present, the LwM2M Client returns to the original no error state where Instance 0 has value 0.
This error code Resource MAY be observed by the LwM2M Server. How to deal with LwM2M Client’s error report depends on the policy of the LwM2M Server. Error codes in between 16 and 32 are specific to the Device and may have different meanings among implementations.]]></Description>
</Item>
<Item ID="12">
<Name>Reset Error Code</Name>
<Operations>E</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type></Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Delete all error code Resource Instances and create only one zero-value error code that implies no error, then re-evaluate all error conditions and update and create Resources Instances to capture all current error conditions.]]></Description>
</Item>
<Item ID="13">
<Name>Current Time</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Time</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Current UNIX time of the LwM2M Client.
The LwM2M Client should be responsible to increase this time value as every second elapses.
The LwM2M Server is able to write this Resource to make the LwM2M Client synchronized with the LwM2M Server.]]></Description>
</Item>
<Item ID="14">
<Name>UTC Offset</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Indicates the UTC offset currently in effect for this LwM2M Device. UTC+X [ISO 8601].]]></Description>
</Item>
<Item ID="15">
<Name>Timezone</Name>
<Operations>RW</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Indicates in which time zone the LwM2M Device is located, in IANA Timezone (TZ) database format.]]></Description>
</Item>
<Item ID="16">
<Name>Supported Binding and Modes</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Indicates which bindings and modes are supported in the LwM2M Client. The possible values are those listed in the LwM2M Core Specification.]]></Description>
</Item>
<Item ID="17"><Name>Device Type</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Type of the device (manufacturer specified string: e.g. smart meters / dev Class / ...)]]></Description>
</Item>
<Item ID="18"><Name>Hardware Version</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Current hardware version of the device]]></Description>
</Item>
<Item ID="19"><Name>Software Version</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Current software version of the device (manufacturer specified string). On elaborated LwM2M device, SW could be split in 2 parts: a firmware one and a higher level software on top.
Both pieces of Software are together managed by LwM2M Firmware Update Object (Object ID 5)]]></Description>
</Item>
<Item ID="20"><Name>Battery Status</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration>0..6</RangeEnumeration>
<Units></Units>
<Description><![CDATA[This value is only valid for the Device Internal Battery if present (one Available Power Sources Resource Instance value is 1).
Battery
Status Meaning Description
0 Normal The battery is operating normally and not on power.
1 Charging The battery is currently charging.
2 Charge Complete The battery is fully charged and still on power.
3 Damaged The battery has some problem.
4 Low Battery The battery is low on charge.
5 Not Installed The battery is not installed.
6 Unknown The battery information is not available.]]></Description>
</Item>
<Item ID="21"><Name>Memory Total</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Integer</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Total amount of storage space which can store data and software in the LwM2M Device (expressed in kilobytes). Note: 1 kilobyte corresponds to 1000 bytes.]]></Description>
</Item>
<Item ID="22"><Name>ExtDevInfo</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>Objlnk</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description><![CDATA[Reference to external "Device" object instance containing information. For example, such an external device can be a Host Device, which is a device into which the Device containing the LwM2M client is embedded. This Resource may be used to retrieve information about the Host Device.]]></Description>
</Item></Resources>
<Description2></Description2>
</Object>
</LWM2M>

2
monitoring/src/main/resources/lwm2m/resource.json

File diff suppressed because one or more lines are too long

2
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -178,6 +178,8 @@ public class ContainerTestSuite {
.waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-coap-transport", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-lwm2m-transport", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT));

2
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -151,6 +151,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices

2
pom.xml

@ -850,6 +850,8 @@
<exclude>.run/**</exclude>
<exclude>**/NetworkReceive.java</exclude>
<exclude>**/lwm2m-registry/**</exclude>
<exclude>**/test/resources/lwm2m/**</exclude>
<exclude>**/resources/lwm2m/models/**</exclude>
<exclude>src/main/data/resources/**</exclude>
</excludes>
<mapping>

16
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -144,12 +144,19 @@ public interface TbContext {
void tellFailure(TbMsg msg, Throwable th);
/**
* Puts new message to queue for processing by the Root Rule Chain
* Puts new message to queue from TbMsg for processing by the Root Rule Chain
*
* @param msg - message
*/
void enqueue(TbMsg msg, Runnable onSuccess, Consumer<Throwable> onFailure);
/**
* Puts new message to custom queue for processing
*
* @param msg - message
*/
void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer<Throwable> onFailure);
/**
* Sends message to the nested rule chain.
* Fails processing of the message if the nested rule chain is not found.
@ -168,13 +175,6 @@ public interface TbContext {
*/
void output(TbMsg msg, String relationType);
/**
* Puts new message to custom queue for processing
*
* @param msg - message
*/
void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer<Throwable> onFailure);
void enqueueForTellFailure(TbMsg msg, String failureMessage);
void enqueueForTellFailure(TbMsg tbMsg, Throwable t);

8
transport/coap/src/main/resources/tb-coap-transport.yml

@ -133,8 +133,6 @@ redis:
transport:
# Local CoAP transport parameters
coap:
# Enable/disable coap transport protocol.
enabled: "${COAP_ENABLED:true}"
# CoaP processing timeout in milliseconds
timeout: "${COAP_TIMEOUT:10000}"
# CoaP piggyback response timeout in milliseconds
@ -173,8 +171,6 @@ transport:
# CoAP server parameters
coap:
# Enable/disable coap transport protocol.
enabled: "${COAP_SERVER_ENABLED:true}"
# CoAP bind-address
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
# CoAP bind port
@ -351,6 +347,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -381,6 +379,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine

4
transport/http/src/main/resources/tb-http-transport.yml

@ -296,6 +296,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -326,6 +328,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine

4
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -397,6 +397,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -427,6 +429,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine

4
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -330,6 +330,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -360,6 +362,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine

4
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -283,6 +283,8 @@ queue:
core:
# Default topic name
topic: "${TB_QUEUE_CORE_TOPIC:tb_core}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}"
# Interval in milliseconds to poll messages by Core microservices
poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}"
# Amount of partitions used by Core microservices
@ -313,6 +315,8 @@ queue:
rule-engine:
# Deprecated. It will be removed in the nearest releases
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}"
# Interval in milliseconds to poll messages by Rule Engine
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
# Timeout for processing a message pack of Rule Engine

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

@ -28,6 +28,8 @@ export interface SysParamsState {
userSettings: UserSettings;
maxResourceSize: number;
maxDebugModeDurationMinutes: number;
maxDataPointsPerRollingArg: number;
maxArgumentsPerCF: number;
ruleChainDebugPerTenantLimitsConfiguration?: string;
}

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

@ -31,6 +31,8 @@ const emptyUserAuthState: AuthPayload = {
persistDeviceStateToTelemetry: false,
mobileQrEnabled: false,
maxResourceSize: 0,
maxArgumentsPerCF: 0,
maxDataPointsPerRollingArg: 0,
maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings
};

4
ui-ngx/src/app/core/http/resource.service.ts

@ -90,8 +90,8 @@ export class ResourceService {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
}
public deleteResource(resourceId: string, config?: RequestConfig) {
return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
public deleteResource(resourceId: string, force = false, config?: RequestConfig) {
return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config));
}
}

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

@ -119,9 +119,22 @@
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div>
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)">
<div class="flex h-9 justify-between">
<button
type="button"
mat-stroked-button
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF"
>
{{ 'calculated-fields.add-argument' | translate }}
</button>
@if (maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.hint.max-args' | translate }}</span>
</div>
}
</div>
</div>

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

@ -30,6 +30,12 @@
}
}
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;

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

@ -55,6 +55,9 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
import { EntityService } from '@core/http/entity.service';
import { MatSort } from '@angular/material/sort';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-calculated-field-arguments-table',
@ -93,6 +96,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentType = ArgumentType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF;
private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
@ -105,6 +109,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
private renderer: Renderer2,
private entityService: EntityService,
private destroyRef: DestroyRef,
private store: Store<AppState>
) {
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => {
this.updateEntityNameMap(value);

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

@ -78,7 +78,7 @@
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.expression' | translate }}</div>
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'action.set' | translate" required>
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" required>
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {
<mat-error>
@if (configFormGroup.get('expressionSIMPLE').hasError('required')) {
@ -89,6 +89,8 @@
{{ 'calculated-fields.hint.expression-max-length' | translate }}
}
</mat-error>
} @else {
<mat-hint>{{ 'calculated-fields.hint.expression' | translate }}</mat-hint>
}
</mat-form-field>
} @else {

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss

@ -40,7 +40,7 @@
&-key {
color: #C52F00;
}
&-ts, &-time-window, &-values, &-value {
&-ts, &-time-window, &-values, &-value, &-func {
color: #7214D0;
}
&-start-ts, &-end-ts, &-limit {

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

@ -166,10 +166,19 @@
formControlName="timeWindow"
/>
</div>
<div class="tb-form-row limit-field-row">
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
<tb-datapoints-limit class="w-full flex-1" formControlName="limit"/>
</div>
@if (maxDataPointsPerRollingArg) {
<div class="tb-form-row limit-field-row">
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
<div class="limit-slider-container flex w-full flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="1" max="{{maxDataPointsPerRollingArg}}">
<input matSliderThumb formControlName="limit" [value]="argumentFormGroup.get('limit').value"/>
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic" appearance="outline">
<input matInput formControlName="limit" type="number" step="1" [value]="argumentFormGroup.get('limit').value" min="1" max="{{maxDataPointsPerRollingArg}}"/>
</mat-form-field>
</div>
</div>
}
}
</div>
</div>

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss

@ -28,9 +28,7 @@ $panel-width: 520px;
min-width: 120px;
}
}
}
:host ::ng-deep {
.limit-field-row {
@media screen and (max-width: $panel-width) {
display: flex;
@ -42,7 +40,9 @@ $panel-width: 520px;
}
}
}
}
:host ::ng-deep {
.time-interval-field {
.advanced-input {
flex-direction: column;

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

@ -38,7 +38,9 @@ import { EntityFilter } from '@shared/models/query/query.models';
import { AliasFilterType } from '@shared/models/alias.models';
import { merge } from 'rxjs';
import { MINUTE } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
@Component({
selector: 'tb-calculated-field-argument-panel',
@ -58,7 +60,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
readonly defaultLimit = Math.max(this.timeService.getMinDatapointsLimit(), Math.floor(this.timeService.getMaxDatapointsLimit() / 10));
readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg;
readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10);
argumentFormGroup = this.fb.group({
argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
@ -72,7 +75,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]],
}),
defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
limit: [this.defaultLimit],
limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }],
timeWindow: [MINUTE * 15],
});
@ -96,7 +99,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
private fb: FormBuilder,
private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>,
private timeService: TimeService
private store: Store<AppState>
) {
this.observeEntityFilterChanges();
this.observeEntityTypeChanges()

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

@ -45,7 +45,7 @@
[scriptLanguage]="ScriptLanguage.TBEL"
[editorCompleter]="data.argumentsEditorCompleter"
resultType="object"
helpId="calculated-field/test-expression_fn"
helpId="calculated-field/expression_fn"
/>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss

@ -78,7 +78,7 @@
&-key {
color: #C52F00;
}
&-ts, &-time-window, &-values, &-value {
&-ts, &-time-window, &-values, &-value, &-func {
color: #7214D0;
}
&-start-ts, &-end-ts, &-limit {

92
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -229,6 +229,92 @@
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">
{{ 'tenant-profile.calculated-fields' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-calculated-fields</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCalculatedFieldsPerEntity"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCalculatedFieldsPerEntity').hasError('required')">
{{ 'tenant-profile.max-calculated-fields-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCalculatedFieldsPerEntity').hasError('min')">
{{ 'tenant-profile.max-calculated-fields-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-data-points-per-rolling-arg</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDataPointsPerRollingArg"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDataPointsPerRollingArg').hasError('required')">
{{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDataPointsPerRollingArg').hasError('min')">
{{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-arguments-per-cf</mat-label>
<input matInput required min="0" step="1"
formControlName="maxArgumentsPerCF"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxArgumentsPerCF').hasError('required')">
{{ 'tenant-profile.max-arguments-per-cf-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxArgumentsPerCF').hasError('min')">
{{ 'tenant-profile.max-arguments-per-cf-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div class="flex-1"></div>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-end" translate>
tenant-profile.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-state-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxStateSizeInKBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxStateSizeInKBytes').hasError('required')">
{{ 'tenant-profile.max-state-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxStateSizeInKBytes').hasError('min')">
{{ 'tenant-profile.max-state-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-value-argument-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSingleValueArgumentSizeInKBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSingleValueArgumentSizeInKBytes').hasError('required')">
{{ 'tenant-profile.max-value-argument-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSingleValueArgumentSizeInKBytes').hasError('min')">
{{ 'tenant-profile.max-value-argument-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">
@ -638,6 +724,12 @@
[type]="rateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT">
</tb-rate-limits>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<tb-rate-limits class="flex-1" formControlName="calculatedFieldDebugEventsRateLimit"
[type]="rateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT">
</tb-rate-limits>
<div class="flex-1"></div>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>

8
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -118,7 +118,13 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
edgeEventRateLimits: [null, []],
edgeEventRateLimitsPerEdge: [null, []],
edgeUplinkMessagesRateLimits: [null, []],
edgeUplinkMessagesRateLimitsPerEdge: [null, []]
edgeUplinkMessagesRateLimitsPerEdge: [null, []],
maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]],
maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]],
maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]],
maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]],
calculatedFieldDebugEventsRateLimit: [null, []],
maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]],
});
this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe(

5
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -45,7 +45,8 @@ export enum RateLimitsType {
EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT',
EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT'
EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT',
CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT = 'CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT',
}
export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
@ -74,6 +75,7 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-per-edge-rate-limit'],
[RateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT, 'tenant-profile.rate-limits.calculated-field-debug-event-rate-limit'],
]
);
@ -103,6 +105,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-per-edge-rate-limit'],
[RateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-calculated-field-debug-event-rate-limit']
]
);

191
ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts

@ -16,6 +16,7 @@
import { Injectable } from '@angular/core';
import {
CellActionDescriptor,
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
@ -27,7 +28,9 @@ import {
ResourceInfo,
ResourceSubType,
ResourceSubTypeTranslationMap,
ResourceType
ResourceType,
ResourceInfoWithReferences,
toResourceDeleteResult
} from '@shared/models/resource.models';
import { EntityType, entityTypeResources } from '@shared/models/entity-type.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -42,8 +45,19 @@ import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component';
import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component';
import { switchMap } from 'rxjs/operators';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component';
import { forkJoin, of } from 'rxjs';
import { parseHttpErrorMessage } from '@core/utils';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import {
ResourcesInUseDialogComponent,
ResourcesInUseDialogData
} from "@shared/components/resource/resources-in-use-dialog.component";
import { ResourcesDatasource } from "@home/pages/admin/resource/resources-datasource";
import { AuthUser } from '@shared/models/user.model';
@Injectable()
export class JsLibraryTableConfigResolver {
@ -53,6 +67,8 @@ export class JsLibraryTableConfigResolver {
constructor(private store: Store<AppState>,
private resourceService: ResourceService,
private translate: TranslateService,
private dialog: MatDialog,
private dialogService: DialogService,
private router: Router,
private datePipe: DatePipe) {
@ -81,20 +97,16 @@ export class JsLibraryTableConfigResolver {
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)),
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('javascript.download'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.downloadResource($event, entity)
}
);
this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store));
this.config.deleteEntityTitle = resource => this.translate.instant('javascript.delete-javascript-resource-title',
{ resourceTitle: resource.title });
this.config.deleteEntityContent = () => this.translate.instant('javascript.delete-javascript-resource-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('javascript.delete-javascript-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('javascript.delete-javascript-resources-text');
this.config.groupActionDescriptors = [{
name: this.translate.instant('action.delete'),
icon: 'delete',
isEnabled: true,
onAction: ($event, entities) => this.deleteResources($event, entities)
}];
this.config.entitiesDeleteEnabled = false;
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, ResourceType.JS_MODULE, this.config.componentsData.resourceSubType);
this.config.loadEntity = id => {
@ -115,7 +127,6 @@ export class JsLibraryTableConfigResolver {
}
return saveObservable;
};
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
this.config.onEntityAction = action => this.onResourceAction(action);
}
@ -126,7 +137,6 @@ export class JsLibraryTableConfigResolver {
resourceSubType: ''
};
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority);
return this.config;
@ -170,4 +180,151 @@ export class JsLibraryTableConfigResolver {
return authority === Authority.SYS_ADMIN;
}
}
private deleteResource($event: Event, resource: ResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('javascript.delete-javascript-resource-title', { resourceTitle: resource.title }),
this.translate.instant('javascript.delete-javascript-resource-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe(
map(() => toResourceDeleteResult(resource)),
catchError((err) => of(toResourceDeleteResult(resource, err)))
).subscribe(
(deleteResult) => {
if (deleteResult.success) {
this.config.updateData();
} else if (deleteResult.resourceIsReferencedError) {
const resources: ResourceInfoWithReferences[] = [{...resource, ...{references: deleteResult.references}}];
const data = {
multiple: false,
resources,
configuration: {
title: 'javascript.javascript-resource-is-in-use',
message: this.translate.instant('javascript.javascript-resource-is-in-use-text', {title: resources[0].title}),
deleteText: 'javascript.delete-javascript-resource-in-use-text',
selectedText: 'javascript.selected-javascript-resources',
columns: ['select', 'title', 'references']
}
};
this.dialog.open<ResourcesInUseDialogComponent, ResourcesInUseDialogData,
ResourceInfo[]>(ResourcesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data
}).afterClosed().subscribe((resources) => {
if (resources) {
this.resourceService.deleteResource(resource.id.id, true).subscribe(
() => {
this.config.updateData();
}
);
}
});
} else {
const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
}
);
}
});
}
private deleteResources($event: Event, resources: ResourceInfo[]) {
if ($event) {
$event.stopPropagation();
}
if (resources && resources.length) {
const title = this.translate.instant('javascript.delete-javascript-resources-title', {count: resources.length});
const content = this.translate.instant('javascript.delete-javascript-resources-text');
this.dialogService.confirm(title, content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((result) => {
if (result) {
const tasks = resources.map((resource) =>
this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe(
map(() => toResourceDeleteResult(resource)),
catchError((err) => of(toResourceDeleteResult(resource, err)))
)
);
forkJoin(tasks).subscribe(
(deleteResults) => {
const anySuccess = deleteResults.some(res => res.success);
const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError);
const otherError = deleteResults.find(res => !res.success);
if (anySuccess) {
this.config.updateData();
}
if (referenceErrors?.length) {
const resourcesWithReferences: ResourceInfoWithReferences[] =
referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}}));
const data = {
multiple: true,
resources: resourcesWithReferences,
configuration: {
title: 'javascript.javascript-resources-are-in-use',
message: this.translate.instant('javascript.javascript-resources-are-in-use-text'),
deleteText: 'javascript.delete-javascript-resource-in-use-text',
selectedText: 'javascript.selected-javascript-resources',
datasource: new ResourcesDatasource(this.resourceService, resourcesWithReferences, entity => true),
columns: ['select', 'title', 'references']
}
};
this.dialog.open<ResourcesInUseDialogComponent, ResourcesInUseDialogData,
ResourceInfo[]>(ResourcesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data
}).afterClosed().subscribe((forceDeleteResources) => {
if (forceDeleteResources && forceDeleteResources.length) {
const forceDeleteTasks = forceDeleteResources.map((resource) =>
this.resourceService.deleteResource(resource.id.id, true)
);
forkJoin(forceDeleteTasks).subscribe(
() => {
this.config.updateData();
}
);
}
});
} else if (otherError) {
const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate);
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'}));
}, errorMessageWithTimeout.timeout);
}
}
);
}
});
}
}
private configureCellActions(authUser: AuthUser): Array<CellActionDescriptor<ResourceInfo>> {
const actions: Array<CellActionDescriptor<ResourceInfo>> = [];
actions.push(
{
name: this.translate.instant('javascript.download'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.downloadResource($event, entity)
},
{
name: this.translate.instant('javascript.delete'),
icon: 'delete',
isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority),
onAction: ($event, entity) => this.deleteResource($event, entity)
},
);
return actions;
}
}

130
ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts

@ -0,0 +1,130 @@
///
/// 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 { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { ResourceInfo, ResourceSubType, ResourceType } from '@shared/models/resource.models';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, take, tap } from 'rxjs/operators';
import { ResourceService } from "@core/http/resource.service";
export class ResourcesDatasource implements DataSource<ResourceInfo> {
private entitiesSubject: Subject<ResourceInfo[]>;
private readonly pageDataSubject: Subject<PageData<ResourceInfo>>;
public pageData$: Observable<PageData<ResourceInfo>>;
public selection = new SelectionModel<ResourceInfo>(true, []);
public dataLoading = true;
constructor(private resourceService: ResourceService,
private resources: ResourceInfo[],
private selectionEnabledFunction: EntityBooleanFunction<ResourceInfo>) {
if (this.resources && this.resources.length) {
this.entitiesSubject = new BehaviorSubject<ResourceInfo[]>(this.resources);
} else {
this.entitiesSubject = new BehaviorSubject<ResourceInfo[]>([]);
this.pageDataSubject = new BehaviorSubject<PageData<ResourceInfo>>(emptyPageData<ResourceInfo>());
this.pageData$ = this.pageDataSubject.asObservable();
}
}
connect(collectionViewer: CollectionViewer):
Observable<ResourceInfo[] | ReadonlyArray<ResourceInfo>> {
return this.entitiesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entitiesSubject.complete();
if (this.pageDataSubject) {
this.pageDataSubject.complete();
}
}
reset() {
this.entitiesSubject.next([]);
if (this.pageDataSubject) {
this.pageDataSubject.next(emptyPageData<ResourceInfo>());
}
}
loadEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable<PageData<ResourceInfo>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<ResourceInfo>>();
this.fetchEntities(pageLink, resourceType, subType).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<ResourceInfo>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;
}
fetchEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable<PageData<ResourceInfo>> {
return this.resourceService.getResources(pageLink, resourceType, subType);
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe(
map((entities) => numSelected === entities.length)
);
}
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
map((entities) => !entities.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
masterToggle() {
this.entitiesSubject.pipe(
tap((entities) => {
const numSelected = this.selection.selected.length;
if (numSelected === this.selectableEntitiesCount(entities)) {
this.selection.clear();
} else {
entities.forEach(row => {
if (this.selectionEnabledFunction(row)) {
this.selection.select(row);
}
});
}
}),
take(1)
).subscribe();
}
private selectableEntitiesCount(entities: Array<ResourceInfo>): number {
return entities.filter((entity) => this.selectionEnabledFunction(entity)).length;
}
}

68
ui-ngx/src/app/shared/components/image/image-gallery.component.ts

@ -16,10 +16,10 @@
import {
ImageResourceInfo,
ImageResourceInfoWithReferences,
ResourceInfoWithReferences,
imageResourceType,
ResourceSubType,
toImageDeleteResult
toResourceDeleteResult
} from '@shared/models/resource.models';
import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { ImageService } from '@core/http/image.service';
@ -67,9 +67,9 @@ import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/
import { ImportExportService } from '@shared/import-export/import-export.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import {
ImagesInUseDialogComponent,
ImagesInUseDialogData
} from '@shared/components/image/images-in-use-dialog.component';
ResourcesInUseDialogComponent,
ResourcesInUseDialogData
} from '@shared/components/resource/resources-in-use-dialog.component';
import { ImagesDatasource } from '@shared/components/image/images-datasource';
import { EmbedImageDialogComponent, EmbedImageDialogData } from '@shared/components/image/embed-image-dialog.component';
@ -504,21 +504,30 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.translate.instant('action.yes')).subscribe((result) => {
if (result) {
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
map(() => toImageDeleteResult(image)),
catchError((err) => of(toImageDeleteResult(image, err)))
map(() => toResourceDeleteResult(image)),
catchError((err) => of(toResourceDeleteResult(image, err)))
).subscribe(
(deleteResult) => {
if (deleteResult.success) {
this.imageDeleted(itemIndex);
} else if (deleteResult.imageIsReferencedError) {
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
} else if (deleteResult.resourceIsReferencedError) {
const images = [{...image, ...{references: deleteResult.references}}];
const data = {
multiple: false,
resources: images,
configuration: {
title: 'image.image-is-in-use',
message: this.translate.instant('image.image-is-in-use-text', {title: images[0].title}),
deleteText: 'image.delete-image-in-use-text',
selectedText: 'image.selected-images',
columns: ['select', 'preview', 'title', 'references']
}
};
this.dialog.open<ResourcesInUseDialogComponent, ResourcesInUseDialogData,
ImageResourceInfo[]>(ResourcesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: false,
images: [{...image, ...{references: deleteResult.references}}]
}
data
}).afterClosed().subscribe((images) => {
if (images) {
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe(
@ -554,29 +563,38 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
if (result) {
const tasks = selectedImages.map((image) =>
this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe(
map(() => toImageDeleteResult(image)),
catchError((err) => of(toImageDeleteResult(image, err)))
map(() => toResourceDeleteResult(image)),
catchError((err) => of(toResourceDeleteResult(image, err)))
)
);
forkJoin(tasks).subscribe(
(deleteResults) => {
const anySuccess = deleteResults.some(res => res.success);
const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError);
const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError);
const otherError = deleteResults.find(res => !res.success);
if (anySuccess) {
this.updateData();
}
if (referenceErrors?.length) {
const imagesWithReferences: ImageResourceInfoWithReferences[] =
referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}}));
this.dialog.open<ImagesInUseDialogComponent, ImagesInUseDialogData,
ImageResourceInfo[]>(ImagesInUseDialogComponent, {
const imagesWithReferences: ResourceInfoWithReferences[] =
referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}}));
const data = {
multiple: true,
resources: imagesWithReferences,
configuration: {
title: 'image.images-are-in-use',
message: this.translate.instant('image.images-are-in-use-text'),
deleteText: 'image.delete-image-in-use-text',
selectedText: 'image.selected-images',
columns: ['select', 'preview', 'title', 'references'],
datasource: new ImagesDatasource(null, imagesWithReferences, entity => true)
}
};
this.dialog.open<ResourcesInUseDialogComponent, ResourcesInUseDialogData,
ImageResourceInfo[]>(ResourcesInUseDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
multiple: true,
images: imagesWithReferences
}
data
}).afterClosed().subscribe((forceDeleteImages) => {
if (forceDeleteImages && forceDeleteImages.length) {
const forceDeleteTasks = forceDeleteImages.map((image) =>

10
ui-ngx/src/app/shared/components/image/image-references.component.ts

@ -17,7 +17,7 @@
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ImageReferences } from '@shared/models/resource.models';
import { ResourceReferences } from '@shared/models/resource.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { getEntityDetailsPageURL } from '@core/utils';
@ -54,7 +54,7 @@ type ReferencedEntitiesEntry = [string, TenantReferencedEntities];
export class ImageReferencesComponent implements OnInit {
@Input()
references: ImageReferences;
references: ResourceReferences;
popoverComponent: TbPopoverComponent<ImageReferencesComponent>;
@ -99,7 +99,7 @@ export class ImageReferencesComponent implements OnInit {
return tenantId === NULL_UUID;
}
private hasNonSystemEntities(references: ImageReferences): boolean {
private hasNonSystemEntities(references: ResourceReferences): boolean {
for (const entityTypeStr of Object.keys(references)) {
const entities = this.references[entityTypeStr];
if (entities.some(e => e.tenantId && e.tenantId.id && e.tenantId.id !== NULL_UUID)) {
@ -109,7 +109,7 @@ export class ImageReferencesComponent implements OnInit {
return false;
}
private toReferencedEntitiesList(references: ImageReferences): ReferencedEntityInfo[] {
private toReferencedEntitiesList(references: ResourceReferences): ReferencedEntityInfo[] {
const result: ReferencedEntityInfo[] = [];
for (const entityTypeStr of Object.keys(references)) {
const entityType = entityTypeStr as EntityType;
@ -127,7 +127,7 @@ export class ImageReferencesComponent implements OnInit {
return result;
}
private toReferencedEntitiesEntries(references: ImageReferences): Observable<ReferencedEntitiesEntry[]> {
private toReferencedEntitiesEntries(references: ResourceReferences): Observable<ReferencedEntitiesEntry[]> {
let referencedEntities: ReferencedEntities = {};
const referencedEntitiesList = this.toReferencedEntitiesList(references);
for (const referencedEntityInfo of referencedEntitiesList) {

42
ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html → ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html

@ -15,10 +15,10 @@
limitations under the License.
-->
<h2 mat-dialog-title>{{title}}</h2>
<div mat-dialog-content class="tb-images-in-use-content" [class]="{multiple: data.multiple}">
<div [innerHTML]="message | safe: 'html'"></div>
<ng-container *ngIf="data.multiple; else singleImageReferences">
<h2 mat-dialog-title translate>{{configuration.title}}</h2>
<div mat-dialog-content class="tb-resources-in-use-content" [class]="{multiple: data.multiple}">
<div [innerHTML]="configuration.message | safe: 'html'"></div>
<ng-container *ngIf="data.multiple; else singleResourceReferences">
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select" sticky>
@ -28,14 +28,14 @@
[indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let image">
<mat-cell *matCellDef="let resource">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? dataSource.selection.toggle(image) : null"
[checked]="dataSource.selection.isSelected(image)">
(change)="$event ? dataSource.selection.toggle(resource) : null"
[checked]="dataSource.selection.isSelected(resource)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="preview">
<ng-container matColumnDef="preview" *ngIf="displayPreview">
<mat-header-cell *matHeaderCellDef style="width: 50px; min-width: 50px;"></mat-header-cell>
<mat-cell *matCellDef="let image">
<img class="tb-image-preview" [src]="image.link | image: {preview: true} | async" alt="{{ image.title }}">
@ -43,30 +43,30 @@
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef style="width: 100%;">
<ng-container *ngIf="dataSource.selection.isEmpty(); else selectedImages">
{{ 'image.name' | translate }}
<ng-container *ngIf="dataSource.selection.isEmpty(); else selectedResources">
{{ 'resource.title' | translate }}
</ng-container>
<ng-template #selectedImages>
{{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }}
<ng-template #selectedResources>
{{ translate.get(configuration.selectedText, {count: dataSource.selection.selected.length}) | async }}
</ng-template>
</mat-header-cell>
<mat-cell *matCellDef="let image">
{{ image.title }}
<mat-cell *matCellDef="let resource">
{{ resource.title }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="references">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let image">
<mat-cell *matCellDef="let resource">
<button #showReferencesButton
mat-stroked-button
color="primary"
(click)="toggleShowReferences($event, image, showReferencesButton)">{{ 'image.references' | translate }}</button>
(click)="toggleShowReferences($event, resource, showReferencesButton)">{{ 'image.references' | translate }}</button>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="['select', 'preview', 'title', 'references']; sticky: true"></mat-header-row>
<mat-header-row class="mat-row-select" *matHeaderRowDef="configuration.columns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select"
[class.mat-selected]="dataSource.selection.isSelected(image)"
*matRowDef="let image; columns: ['select', 'preview', 'title', 'references'];"></mat-row>
[class.mat-selected]="dataSource.selection.isSelected(resource)"
*matRowDef="let resource; columns: configuration.columns;"></mat-row>
</table>
</div>
</ng-container>
@ -76,7 +76,7 @@
<button mat-raised-button color="accent" (click)="delete()" [disabled]="data.multiple && dataSource.selection.isEmpty()"
cdkFocusInitial>{{ (data.multiple ? 'action.delete-selected' : 'action.delete-anyway') | translate}}</button>
</div>
<ng-template #singleImageReferences>
<ng-template #singleResourceReferences>
<tb-image-references [references]="references"></tb-image-references>
<div [innerHTML]="'image.delete-image-in-use-text' | translate | safe: 'html'"></div>
<div [innerHTML]="configuration.deleteText | translate | safe: 'html'"></div>
</ng-template>

2
ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss → ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss

@ -14,7 +14,7 @@
* limitations under the License.
*/
:host {
.tb-images-in-use-content {
.tb-resources-in-use-content {
display: flex;
flex-direction: column;
gap: 24px;

61
ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts → ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts

@ -20,37 +20,50 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageReferences, ImageResourceInfo, ImageResourceInfoWithReferences } from '@shared/models/resource.models';
import { ImagesDatasource } from '@shared/components/image/images-datasource';
import {
ResourceReferences,
ResourceInfoWithReferences,
ResourceInfo
} from '@shared/models/resource.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ImageReferencesComponent } from '@shared/components/image/image-references.component';
import { TranslateService } from '@ngx-translate/core';
import { Datasource } from "@shared/models/widget.models";
export interface ImagesInUseDialogData {
interface ResourcesInUseDialogDataConfiguration {
title: string;
message: string;
columns: string[];
deleteText: string;
selectedText: string;
datasource?: Datasource;
}
export interface ResourcesInUseDialogData {
multiple: boolean;
images: ImageResourceInfoWithReferences[];
resources: ResourceInfoWithReferences[];
configuration: ResourcesInUseDialogDataConfiguration;
}
@Component({
selector: 'tb-images-in-use-dialog',
templateUrl: './images-in-use-dialog.component.html',
styleUrls: ['./images-in-use-dialog.component.scss']
selector: 'tb-resources-in-use-dialog',
templateUrl: './resources-in-use-dialog.component.html',
styleUrls: ['./resources-in-use-dialog.component.scss']
})
export class ImagesInUseDialogComponent extends
DialogComponent<ImagesInUseDialogComponent, ImageResourceInfo[]> implements OnInit {
title: string;
message: string;
export class ResourcesInUseDialogComponent extends
DialogComponent<ResourcesInUseDialogComponent, ResourceInfo[]> implements OnInit {
references: ImageReferences;
displayPreview: boolean;
configuration: ResourcesInUseDialogDataConfiguration;
references: ResourceReferences;
dataSource: ImagesDatasource;
dataSource: Datasource;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ImagesInUseDialogData,
public dialogRef: MatDialogRef<ImagesInUseDialogComponent, ImageResourceInfo[]>,
@Inject(MAT_DIALOG_DATA) public data: ResourcesInUseDialogData,
public dialogRef: MatDialogRef<ResourcesInUseDialogComponent, ResourceInfo[]>,
public translate: TranslateService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
@ -59,14 +72,12 @@ export class ImagesInUseDialogComponent extends
}
ngOnInit(): void {
this.configuration = this.data.configuration;
this.displayPreview = this.data.configuration.columns.includes('preview');
if (this.data.multiple) {
this.title = this.translate.instant('image.images-are-in-use');
this.message = this.translate.instant('image.images-are-in-use-text');
this.dataSource = new ImagesDatasource(null, this.data.images, entity => true);
this.dataSource = this.data.configuration.datasource;
} else {
this.title = this.translate.instant('image.image-is-in-use');
this.message = this.translate.instant('image.image-is-in-use-text', {title: this.data.images[0].title});
this.references = this.data.images[0].references;
this.references = this.data.resources[0].references;
}
}
@ -78,11 +89,11 @@ export class ImagesInUseDialogComponent extends
if (this.data.multiple) {
this.dialogRef.close(this.dataSource.selection.selected);
} else {
this.dialogRef.close(this.data.images);
this.dialogRef.close(this.data.resources);
}
}
toggleShowReferences($event: Event, image: ImageResourceInfoWithReferences, referencesButton: MatButton) {
toggleShowReferences($event: Event, resource: ResourceInfoWithReferences, referencesButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
@ -93,7 +104,7 @@ export class ImagesInUseDialogComponent extends
const referencesPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ImageReferencesComponent, 'top', true, null,
{
references: image.references
references: resource.references
}, {}, {}, {},
false,
visible => {

156
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -29,6 +29,7 @@ import { AliasFilterType } from '@shared/models/alias.models';
import { Observable } from 'rxjs';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import {
AceHighlightRule,
AceHighlightRules,
dotOperatorHighlightRule,
endGroupHighlightRule
@ -268,12 +269,159 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = {
}
},
};
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
max: {
meta: 'function',
description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The maximum value, or NaN if applicable',
type: 'number'
}
},
min: {
meta: 'function',
description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The minimum value, or NaN if applicable',
type: 'number'
}
},
mean: {
meta: 'function',
description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The mean value, or NaN if applicable',
type: 'number'
}
},
std: {
meta: 'function',
description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The standard deviation, or NaN if applicable',
type: 'number'
}
},
median: {
meta: 'function',
description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The median value, or NaN if applicable',
type: 'number'
}
},
count: {
meta: 'function',
description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The count of values',
type: 'number'
}
},
last: {
meta: 'function',
description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The last value, or NaN if applicable',
type: 'number'
}
},
first: {
meta: 'function',
description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The first value, or NaN if applicable',
type: 'number'
}
},
sum: {
meta: 'function',
description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The sum of values, or NaN if applicable',
type: 'number'
}
}
};
export const CalculatedFieldRollingValueArgumentAutocomplete = {
meta: 'object',
type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }',
description: 'Calculated field rolling value argument.',
children: {
...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
values: {
meta: 'array',
type: '{ ts: number; value: any; }[]',
@ -355,6 +503,13 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
],
}
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({
token: 'tb.calculated-field-func',
regex: `\\b${funcName}\\b`,
next: 'no_regex'
}));
const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = {
calculatedFieldRollingArgumentValue: [
dotOperatorHighlightRule,
@ -368,6 +523,7 @@ const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = {
regex: /timeWindow/,
next: 'calculatedFieldRollingArgumentTimeWindow'
},
...calculatedFieldRollingArgumentValueFunctionsHighlightRules,
endGroupHighlightRule
],
}

24
ui-ngx/src/app/shared/models/resource.models.ts

@ -120,28 +120,28 @@ export interface ImageExportData {
export type ImageResourceType = 'tenant' | 'system';
export type TBResourceScope = 'tenant' | 'system';
export type ImageReferences = {[entityType: string]: Array<BaseData<HasId> & HasTenantId>};
export type ResourceReferences = {[entityType: string]: Array<BaseData<HasId> & HasTenantId>};
export interface ImageResourceInfoWithReferences extends ImageResourceInfo {
references: ImageReferences;
export interface ResourceInfoWithReferences extends ResourceInfo {
references: ResourceReferences;
}
export interface ImageDeleteResult {
image: ImageResourceInfo;
export interface ResourceDeleteResult {
resource: TbResourceInfo<any>;
success: boolean;
imageIsReferencedError?: boolean;
resourceIsReferencedError?: boolean;
error?: any;
references?: ImageReferences;
references?: ResourceReferences;
}
export const toImageDeleteResult = (image: ImageResourceInfo, e?: any): ImageDeleteResult => {
export const toResourceDeleteResult = (resource: ResourceInfo, e?: any): ResourceDeleteResult => {
if (!e) {
return {image, success: true};
return {resource, success: true};
} else {
const result: ImageDeleteResult = {image, success: false, error: e};
const result: ResourceDeleteResult = {resource, success: false, error: e};
if (e?.status === 400 && e?.error?.success === false && e?.error?.references) {
const references: ImageReferences = e?.error?.references;
result.imageIsReferencedError = true;
const references: ResourceReferences = e?.error?.references;
result.resourceIsReferencedError = true;
result.references = references;
}
return result;

15
ui-ngx/src/app/shared/models/tenant.model.ts

@ -95,6 +95,13 @@ export interface DefaultTenantProfileConfiguration {
rpcTtlDays: number;
queueStatsTtlDays: number;
ruleEngineExceptionsTtlDays: number;
maxCalculatedFieldsPerEntity: number;
maxArgumentsPerCF: number;
maxDataPointsPerRollingArg: number;
maxStateSizeInKBytes: number;
maxSingleValueArgumentSizeInKBytes: number;
calculatedFieldDebugEventsRateLimit: string;
}
export type TenantProfileConfigurations = DefaultTenantProfileConfiguration;
@ -148,7 +155,13 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
alarmsTtlDays: 0,
rpcTtlDays: 0,
queueStatsTtlDays: 0,
ruleEngineExceptionsTtlDays: 0
ruleEngineExceptionsTtlDays: 0,
maxCalculatedFieldsPerEntity: 0,
maxArgumentsPerCF: 0,
maxDataPointsPerRollingArg: 0,
maxStateSizeInKBytes: 0,
maxSingleValueArgumentSizeInKBytes: 0,
calculatedFieldDebugEventsRateLimit: ''
};
configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};
break;

6
ui-ngx/src/app/shared/shared.module.ts

@ -201,7 +201,7 @@ import { ImageGalleryComponent } from '@shared/components/image/image-gallery.co
import { UploadImageDialogComponent } from '@shared/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@shared/components/image/image-dialog.component';
import { ImageReferencesComponent } from '@shared/components/image/image-references.component';
import { ImagesInUseDialogComponent } from '@shared/components/image/images-in-use-dialog.component';
import { ResourcesInUseDialogComponent } from '@shared/components/resource/resources-in-use-dialog.component';
import { GalleryImageInputComponent } from '@shared/components/image/gallery-image-input.component';
import { MultipleGalleryImageInputComponent } from '@shared/components/image/multiple-gallery-image-input.component';
import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component';
@ -426,7 +426,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
ResourcesInUseDialogComponent,
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
@ -690,7 +690,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
ResourcesInUseDialogComponent,
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,

1
ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md

@ -1 +0,0 @@
<!-- [TODO]: [Calculated Fields] add content -->

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1068,7 +1068,9 @@
"argument-name-pattern": "Argument name is invalid.",
"argument-name-duplicate": "Argument with such name already exists.",
"argument-name-max-length": "Argument name should be less than 256 characters.",
"argument-type-required": "Argument type is required."
"argument-type-required": "Argument type is required.",
"max-args": "Maximum number of arguments reached.",
"expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius."
}
},
"confirm-on-exit": {
@ -4362,6 +4364,7 @@
"delete-javascript-resources-action-title": "Delete JavaScript { count, plural, =1 {1 resource} other {# resources} }",
"delete-javascript-resources-text": "Please note that the selected JavaScript resources, even if they are used in JavaScript functions, will be deleted.",
"delete-javascript-resources-title": "Are you sure you want to delete JavaScript { count, plural, =1 {1 resource} other {# resources} }?",
"delete-javascript-resource-in-use-text": "If you still want to delete the JavaScript resource, click the <b>Delete anyway</b> button.",
"download": "Download JavaScript resource",
"upload-from-file": "Upload JavaScript from file",
"resource-file": "JavaScript resource file",
@ -4370,6 +4373,10 @@
"javascript-library": "JavaScript library",
"javascript-type": "JavaScript type",
"javascript-resource-details": "JavaScript resource details",
"javascript-resource-is-in-use": "JavaScript resource is used by other entities",
"javascript-resources-are-in-use": "JavaScript resources are used by other entities",
"javascript-resource-is-in-use-text": "The JavaScript resource <b>'{{title}}'</b> was not deleted because it is used by the following entities:",
"javascript-resources-are-in-use-text": "Not all JavaScript resources have been deleted because they are used by other entities.</br>You can view referenced entities by clicking the <b>References</b> button in the corresponding resource row.</br>If you still want to delete these JavaScript resources, select them in the table below and click the <b>Delete selected</b> button.",
"search": "Search JavaScript resources",
"selected-javascript-resources": "{ count, plural, =1 {1 JavaScript resource} other {# JavaScript resources} } selected",
"no-javascript-resource-text": "No JavaScript resources found",
@ -5474,6 +5481,7 @@
"entities": "Entities",
"rule-engine": "Rule Engine",
"time-to-live": "Time-to-live",
"calculated-fields": "Calculated fields",
"alarms-and-notifications": "Alarms and notifications",
"ota-files-in-bytes": "Files",
"ws-title": "WS",
@ -5526,6 +5534,21 @@
"tenant-entity-import-rate-limit": "Entity version load",
"tenant-notification-request-rate-limit": "Notification requests",
"tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule",
"max-calculated-fields": "Maximum number of calculated fields per entity",
"max-calculated-fields-range": "Maximum number of calculated fields per entity can't be negative",
"max-calculated-fields-required": "Maximum number of calculated fields per entity is required",
"max-data-points-per-rolling-arg": "Maximum number of data points in a time series rolling arguments",
"max-data-points-per-rolling-arg-range": "Maximum number of data points in a time series rolling arguments can't be negative",
"max-data-points-per-rolling-arg-required": "Maximum number of data points in a time series rolling arguments is required",
"max-arguments-per-cf": "Maximum number of arguments per calculated field",
"max-arguments-per-cf-range": "Maximum number of arguments per calculated field can't be negative",
"max-arguments-per-cf-required": "Maximum number of arguments per calculated field is required",
"max-state-size": "Maximum size of the state in KB",
"max-state-size-range": "Maximum size of the state in KB can't be negative",
"max-state-size-required": "Maximum size of the state in KB is required",
"max-value-argument-size": "Maximum size of the single value argument in KB",
"max-value-argument-size-range": "Maximum size of the single value argument in KB can't be negative",
"max-value-argument-size-required": "Maximum size of the single value argument in KB is required",
"max-transport-messages": "Transport messages maximum number",
"max-transport-messages-required": "Transport messages maximum number is required.",
"max-transport-messages-range": "Transport messages maximum number can't be negative",
@ -5597,6 +5620,8 @@
"advanced-settings": "Advanced settings",
"edit-limit": "Edit limit",
"but-less-than": "but less than",
"calculated-field-debug-event-rate-limit": "Calculated field debug events",
"edit-calculated-field-debug-event-rate-limit": "Edit calculated field debug events rate limits",
"edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits",
"edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits",
"edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits",

Loading…
Cancel
Save