Browse Source

Merge remote-tracking branch 'upstream/master' into feature/calculated-fields-vc

pull/12707/head
Andrii Landiak 2 years ago
parent
commit
324bcc1cbf
  1. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 5
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 30
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  4. 9
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  7. 11
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  8. 5
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  9. 134
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  10. 74
      application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
  11. 202
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  12. 144
      application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java
  13. 123
      application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
  14. 221
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  15. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  16. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java
  17. 9
      common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java
  18. 32
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
  19. 5
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  20. 1
      common/proto/src/main/proto/queue.proto
  21. 38
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  22. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  23. 60
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
  24. 11
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  25. 29
      dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
  26. 3
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java
  27. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java
  28. 3
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  29. 12
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  30. 60
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  31. 15
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  32. 20
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  33. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
  34. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
  35. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  36. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
  37. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  38. 1
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  39. 5
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  40. 1
      dao/src/main/resources/sql/schema-entities.sql
  41. 47
      dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java
  42. 2
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  43. 2
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  44. 16
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  45. 4
      transport/coap/src/main/resources/tb-coap-transport.yml
  46. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  47. 2
      ui-ngx/src/app/core/auth/auth.reducer.ts
  48. 4
      ui-ngx/src/app/core/http/resource.service.ts
  49. 17
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  50. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  51. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  52. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  53. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss
  54. 17
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  55. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss
  56. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  57. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  58. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss
  59. 92
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  60. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  61. 5
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  62. 191
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  63. 130
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts
  64. 68
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  65. 10
      ui-ngx/src/app/shared/components/image/image-references.component.ts
  66. 42
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html
  67. 2
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss
  68. 61
      ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts
  69. 156
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  70. 24
      ui-ngx/src/app/shared/models/resource.models.ts
  71. 15
      ui-ngx/src/app/shared/models/tenant.model.ts
  72. 6
      ui-ngx/src/app/shared/shared.module.ts
  73. 1
      ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md
  74. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

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,

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

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

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

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

@ -1142,7 +1142,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) {
@ -1158,6 +1160,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 {

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

@ -1299,7 +1299,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;
}

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

@ -59,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);
}
};

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
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -1211,7 +1211,7 @@ public class DeviceServiceTest extends AbstractServiceTest {
}
@Test
public void testDeleteAssetIfReferencedInCalculatedField() {
public void testDeleteDeviceIfReferencedInCalculatedField() {
Device device = saveDevice(tenantId, "Test Device");
Device deviceWithCf = saveDevice(tenantId, "Device with CF");

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

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

4
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

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