Browse Source

Merge branch 'develop/3.5.2' into feature/basic-widget-config

pull/9091/head
Igor Kulikov 3 years ago
parent
commit
95f07b3994
  1. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  2. 22
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  5. 18
      application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
  6. 12
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  7. 5
      application/src/main/resources/thingsboard.yml
  8. 13
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  9. 93
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  10. 98
      application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackCallbackTest.java
  11. 215
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  12. 41
      common/data/src/main/java/org/thingsboard/server/common/data/exception/AbstractRateLimitException.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java
  14. 10
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java
  16. 41
      common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java
  17. 18
      common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java
  18. 73
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  19. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java
  20. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java
  21. 3
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java
  22. 2
      common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java
  23. 2
      common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java
  24. 2
      common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java
  25. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java
  26. 28
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
  27. 54
      common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplateTest.java
  28. 20
      common/queue/src/test/resources/logback-test.xml
  29. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  30. 19
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  31. 11
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  32. 161
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapTransportResourceTest.java
  33. 67
      common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java
  34. 74
      common/util/src/test/java/org/thingsboard/common/util/ExceptionUtilTest.java
  35. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  36. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  37. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  38. 25
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  39. 25
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java
  40. 2
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  41. 4
      netty-mqtt/pom.xml
  42. 108
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  43. 7
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
  44. 6
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
  45. 15
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  46. 3
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java
  47. 2
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java
  48. 17
      netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java
  49. 22
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  50. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
  51. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java
  52. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  53. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java
  54. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java
  55. 2
      transport/coap/src/main/resources/tb-coap-transport.yml
  56. 2
      transport/http/src/main/resources/tb-http-transport.yml
  57. 2
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  58. 2
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  59. 2
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  60. 12
      ui-ngx/src/app/core/http/alarm.service.ts
  61. 18
      ui-ngx/src/app/core/http/attribute.service.ts
  62. 128
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  63. 8
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  64. 92
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  65. 68
      ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html
  66. 44
      ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss
  67. 151
      ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts
  68. 4
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  69. 2
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  70. 70
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html
  71. 44
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss
  72. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  73. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  74. 4
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  75. 4
      ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.scss
  76. 5
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss
  77. 13
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html
  78. 48
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  79. 9
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.html
  80. 57
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts
  81. 10
      ui-ngx/src/app/shared/components/help-popup.component.html
  82. 18
      ui-ngx/src/app/shared/components/help-popup.component.scss
  83. 6
      ui-ngx/src/app/shared/components/help-popup.component.ts
  84. 3
      ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html
  85. 24
      ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts
  86. 10
      ui-ngx/src/app/shared/components/string-items-list.component.html
  87. 18
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  88. 1
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  89. 1
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  90. 1
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  91. 1
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  92. 1
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  93. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  94. 1
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  95. 1
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json
  96. 1
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  97. 1
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  98. 1
      ui-ngx/src/assets/locale/locale.constant-ja_JP.json
  99. 1
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  100. 1
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -232,7 +232,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
} catch (RuleNodeException rne) { } catch (RuleNodeException rne) {
msg.getCallback().onFailure(rne); msg.getCallback().onFailure(rne);
} catch (Exception e) { } catch (Exception e) {
msg.getCallback().onFailure(new RuleEngineException(e.getMessage())); msg.getCallback().onFailure(new RuleEngineException(e.getMessage(), e));
} }
} }
@ -335,7 +335,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
msg.getCallback().onFailure(rne); msg.getCallback().onFailure(rne);
} catch (Exception e) { } catch (Exception e) {
log.warn("[" + tenantId + "]" + "[" + entityId + "]" + "[" + msg.getId() + "]" + " onTellNext failure", e); log.warn("[" + tenantId + "]" + "[" + entityId + "]" + "[" + msg.getId() + "]" + " onTellNext failure", e);
msg.getCallback().onFailure(new RuleEngineException("onTellNext - " + e.getMessage())); msg.getCallback().onFailure(new RuleEngineException("onTellNext - " + e.getMessage(), e));
} }
} }

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

@ -201,7 +201,7 @@ public class TelemetryController extends BaseController {
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException { @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope));
} }
@ApiOperation(value = "Get attributes (getAttributes)", @ApiOperation(value = "Get attributes (getAttributes)",
@ -219,9 +219,9 @@ public class TelemetryController extends BaseController {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
} }
@ -245,7 +245,7 @@ public class TelemetryController extends BaseController {
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
} }
@ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)",
@ -259,7 +259,7 @@ public class TelemetryController extends BaseController {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType, @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException { @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor()));
} }
@ApiOperation(value = "Get latest time-series value (getLatestTimeseries)", @ApiOperation(value = "Get latest time-series value (getLatestTimeseries)",
@ -462,7 +462,9 @@ public class TelemetryController extends BaseController {
notes = "Delete time-series for selected entity based on entity id, entity type and keys." + notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
" Use 'deleteAllDataForKeys' to delete all time-series data." + " Use 'deleteAllDataForKeys' to delete all time-series data." +
" Use 'startTs' and 'endTs' to specify time-range instead. " + " Use 'startTs' and 'endTs' to specify time-range instead. " +
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) after deletion of the time range. " + " Use 'deleteLatest' to delete latest value (stored in separate table for performance) if the value's timestamp matches the time-range. " +
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." +
" The replacement value will be fetched from the 'time-series' table, and its timestamp will be the most recent one before the defined time-range. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE) produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = { @ApiResponses(value = {
@ -486,14 +488,16 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "startTs", required = false) Long startTs, @RequestParam(name = "startTs", required = false) Long startTs,
@ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.") @ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.")
@RequestParam(name = "endTs", required = false) Long endTs, @RequestParam(name = "endTs", required = false) Long endTs,
@ApiParam(value = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.")
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
@ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") @ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
} }
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException { Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException {
List<String> keys = toKeysList(keysStr); List<String> keys = toKeysList(keysStr);
if (keys.isEmpty()) { if (keys.isEmpty()) {
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
@ -517,7 +521,7 @@ public class TelemetryController extends BaseController {
return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> { return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> {
List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>(); List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
for (String key : keys) { for (String key : keys) {
deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted)); deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted, deleteLatest));
} }
tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() { tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() {
@Override @Override

4
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -756,6 +756,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) { } catch (Exception e) {
} }
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
} }

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -363,7 +363,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
callback.onSuccess(); callback.onSuccess();
} }
} catch (Exception e) { } catch (Exception e) {
callback.onFailure(new RuleEngineException(e.getMessage())); callback.onFailure(new RuleEngineException(e.getMessage(), e));
} }
} }

18
application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java

@ -17,11 +17,14 @@ package org.thingsboard.server.service.queue;
import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ExceptionUtil;
import org.thingsboard.server.common.data.exception.AbstractRateLimitException;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -57,8 +60,23 @@ public class TbMsgPackCallback implements TbMsgCallback {
ctx.onSuccess(id); ctx.onSuccess(id);
} }
@Override
public void onRateLimit(RuleEngineException e) {
log.debug("[{}] ON RATE LIMIT", id, e);
//TODO notify tenant on rate limit
if (failedMsgTimer != null) {
failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS);
}
ctx.onSuccess(id);
}
@Override @Override
public void onFailure(RuleEngineException e) { public void onFailure(RuleEngineException e) {
if (ExceptionUtil.lookupExceptionInCause(e, AbstractRateLimitException.class) != null) {
onRateLimit(e);
return;
}
log.trace("[{}] ON FAILURE", id, e); log.trace("[{}] ON FAILURE", id, e);
if (failedMsgTimer != null) { if (failedMsgTimer != null) {
failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS); failedMsgTimer.record(System.currentTimeMillis() - startMsgProcessing, TimeUnit.MILLISECONDS);

12
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -228,7 +228,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
save(deviceId, LAST_CONNECT_TIME, ts); save(deviceId, LAST_CONNECT_TIME, ts);
pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT); pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData); checkAndUpdateState(deviceId, stateData);
} }
@Override @Override
@ -397,12 +396,17 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
void checkAndUpdateState(@Nonnull DeviceId deviceId, @Nonnull DeviceStateData state) { void checkAndUpdateState(@Nonnull DeviceId deviceId, @Nonnull DeviceStateData state) {
if (state.getState().isActive()) { var deviceState = state.getState();
if (deviceState.isActive()) {
updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, state); updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, state);
} else { } else {
//trying to fix activity state //trying to fix activity state
if (isActive(System.currentTimeMillis(), state.getState())) { if (isActive(System.currentTimeMillis(), deviceState)) {
updateActivityState(deviceId, state, state.getState().getLastActivityTime()); updateActivityState(deviceId, state, deviceState.getLastActivityTime());
if (deviceState.getLastInactivityAlarmTime() != 0L && deviceState.getLastInactivityAlarmTime() >= deviceState.getLastActivityTime()) {
deviceState.setLastInactivityAlarmTime(0L);
save(deviceId, INACTIVITY_ALARM_TIME, 0L);
}
} }
} }
} }

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

@ -96,7 +96,10 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" # The recalculate_delay property recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
# The delay is recommended because the initialization of rule chain actors is time-consuming. Avoiding unnecessary recalculations during a restart can enhance system performance and stability.
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cluster: cluster:
stats: stats:

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

@ -796,6 +796,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
} }
protected <T> T doDeleteAsync(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doDeleteAsync(urlTemplate, DEFAULT_TIMEOUT, params).andExpect(status().isOk()), responseClass);
}
protected ResultActions doPost(String urlTemplate, String... params) throws Exception { protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
MockHttpServletRequestBuilder postRequest = post(urlTemplate); MockHttpServletRequestBuilder postRequest = post(urlTemplate);
setJwtToken(postRequest); setJwtToken(postRequest);
@ -828,6 +832,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(deleteRequest); return mockMvc.perform(deleteRequest);
} }
protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception {
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params);
setJwtToken(deleteRequest);
// populateParams(deleteRequest, params);
MvcResult result = mockMvc.perform(deleteRequest).andReturn();
result.getAsyncResult(timeout);
return mockMvc.perform(asyncDispatch(result));
}
protected void populateParams(MockHttpServletRequestBuilder request, String... params) { protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
if (params != null && params.length > 0) { if (params != null && params.length > 0) {
Assert.assertEquals(0, params.length % 2); Assert.assertEquals(0, params.length % 2);

93
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -15,15 +15,22 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
@ -44,6 +51,92 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
} }
@Test
public void testDeleteAllTelemetryWithLatest() throws Exception {
loginTenantAdmin();
Device device = createDevice();
SingleEntityFilter filter = new SingleEntityFilter();
filter.setSingleEntity(device.getId());
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
getWsClient().registerWaitForUpdate(1);
long startTs = System.currentTimeMillis();
String testBody = "{\"data\": \"value\"}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
long endTs = System.currentTimeMillis();
ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
Assert.assertNotNull(latest);
var data = latest.get("data");
Assert.assertNotNull(data);
Assert.assertEquals("value", data.get(0).get("value").asText());
ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
Assert.assertNotNull(timeseries);
Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText());
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true", String.class);
latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
Assert.assertTrue(latest.get("data").get(0).get("value").isNull());
timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
Assert.assertTrue(timeseries.isEmpty());
}
@Test
public void testDeleteAllTelemetryWithoutLatest() throws Exception {
loginTenantAdmin();
Device device = createDevice();
SingleEntityFilter filter = new SingleEntityFilter();
filter.setSingleEntity(device.getId());
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
getWsClient().registerWaitForUpdate(1);
long startTs = System.currentTimeMillis();
String testBody = "{\"data\": \"value\"}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
long endTs = System.currentTimeMillis();
ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
Assert.assertNotNull(latest);
Assert.assertEquals("value", latest.get("data").get(0).get("value").asText());
ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
Assert.assertNotNull(timeseries);
Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText());
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true&deleteLatest=false", String.class);
latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
Assert.assertEquals("value", latest.get("data").get(0).get("value").asText());
timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
Assert.assertTrue(timeseries.isEmpty());
}
@Test @Test
public void testValueConstraintValidator() throws Exception { public void testValueConstraintValidator() throws Exception {
loginTenantAdmin(); loginTenantAdmin();

98
application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackCallbackTest.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2023 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.queue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import java.util.UUID;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
class TbMsgPackCallbackTest {
TenantId tenantId;
UUID msgId;
TbMsgPackProcessingContext ctx;
TbMsgPackCallback callback;
@BeforeEach
void setUp() {
tenantId = TenantId.fromUUID(UUID.randomUUID());
msgId = UUID.randomUUID();
ctx = mock(TbMsgPackProcessingContext.class);
callback = spy(new TbMsgPackCallback(msgId, tenantId, ctx));
}
private static Stream<Arguments> testOnFailure_NotRateLimitException() {
return Stream.of(
Arguments.of(new RuleEngineException("rule engine no cause")),
Arguments.of(new RuleEngineException("rule engine caused 1 lvl", new RuntimeException())),
Arguments.of(new RuleEngineException("rule engine caused 2 lvl", new RuntimeException(new Exception()))),
Arguments.of(new RuleEngineException("rule engine caused 2 lvl Throwable", new RuntimeException(new Throwable()))),
Arguments.of(new RuleNodeException("rule node no cause", "RuleChain", new RuleNode()))
);
}
@ParameterizedTest
@MethodSource
void testOnFailure_NotRateLimitException(RuleEngineException ree) {
callback.onFailure(ree);
verify(callback, never()).onRateLimit(any());
verify(callback, never()).onSuccess();
verify(ctx, never()).onSuccess(any());
}
private static Stream<Arguments> testOnFailure_RateLimitException() {
return Stream.of(
Arguments.of(new RuleEngineException("caused lvl 1", new TbRateLimitsException(EntityType.ASSET))),
Arguments.of(new RuleEngineException("caused lvl 2", new RuntimeException(new TbRateLimitsException(EntityType.ASSET)))),
Arguments.of(
new RuleEngineException("caused lvl 3",
new RuntimeException(
new Exception(
new TbRateLimitsException(EntityType.ASSET)))))
);
}
@ParameterizedTest
@MethodSource
void testOnFailure_RateLimitException(RuleEngineException ree) {
callback.onFailure(ree);
verify(callback).onRateLimit(any());
verify(callback).onFailure(any());
verify(callback, never()).onSuccess();
verify(ctx).onSuccess(msgId);
verify(ctx).onSuccess(any());
verify(ctx, never()).onFailure(any(), any(), any());
}
}

215
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -22,19 +22,30 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -62,14 +73,32 @@ public class DefaultDeviceStateServiceTest {
PartitionService partitionService; PartitionService partitionService;
@Mock @Mock
DeviceStateData deviceStateDataMock; DeviceStateData deviceStateDataMock;
@Mock
EntityQueryRepository entityQueryRepository;
TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"));
DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"); DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112");
TopicPartitionInfo tpi;
DefaultDeviceStateService service; DefaultDeviceStateService service;
TelemetrySubscriptionService telemetrySubscriptionService;
@Before @Before
public void setUp() { public void setUp() {
service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class))); service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, null, mock(NotificationRuleProcessor.class)));
telemetrySubscriptionService = Mockito.mock(TelemetrySubscriptionService.class);
ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService);
ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60);
ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60);
ReflectionTestUtils.setField(service, "initFetchPackSize", 10);
tpi = TopicPartitionInfo.builder().myPartition(true).build();
Mockito.when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi);
Mockito.when(entityQueryRepository.findEntityDataByQueryInternal(Mockito.any())).thenReturn(new PageData<>());
var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId());
Mockito.when(deviceService.findDeviceIdInfos(Mockito.any()))
.thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false));
} }
@Test @Test
@ -125,4 +154,188 @@ public class DefaultDeviceStateServiceTest {
Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout()); Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout());
} }
private void initStateService(long timeout) throws InterruptedException {
service.stop();
Mockito.reset(service, telemetrySubscriptionService);
ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", timeout);
service.init();
PartitionChangeEvent event = new PartitionChangeEvent(this, new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi));
service.onApplicationEvent(event);
Thread.sleep(100);
}
@Test
public void increaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(true);
Thread.sleep(increase);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout + 5);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseInactivityForActiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Mockito.reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
Thread.sleep(defaultTimeout + increase);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseSmallInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long newTimeout = 1;
Thread.sleep(newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
}
@Test
public void decreaseInactivityForActiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Mockito.reset(telemetrySubscriptionService);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
long newTimeout = 1;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout);
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void decreaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long newTimeout = 1;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
}
private void activityVerify(boolean isActive) {
Mockito.verify(telemetrySubscriptionService, Mockito.times(1)).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.eq(isActive), Mockito.any());
}
} }

41
common/data/src/main/java/org/thingsboard/server/common/data/exception/AbstractRateLimitException.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 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.exception;
public abstract class AbstractRateLimitException extends RuntimeException {
public AbstractRateLimitException() {
super();
}
public AbstractRateLimitException(String message) {
super(message);
}
public AbstractRateLimitException(String message, Throwable cause) {
super(message, cause);
}
public AbstractRateLimitException(Throwable cause) {
super(cause);
}
protected AbstractRateLimitException(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/exception/ApiUsageLimitsExceededException.java

@ -15,7 +15,7 @@
*/ */
package org.thingsboard.server.common.data.exception; package org.thingsboard.server.common.data.exception;
public class ApiUsageLimitsExceededException extends RuntimeException { public class ApiUsageLimitsExceededException extends AbstractRateLimitException {
public ApiUsageLimitsExceededException(String message) { public ApiUsageLimitsExceededException(String message) {
super(message); super(message);
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseDeleteTsKvQuery.java

@ -21,14 +21,20 @@ import lombok.Data;
public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuery { public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuery {
private final Boolean rewriteLatestIfDeleted; private final Boolean rewriteLatestIfDeleted;
private final Boolean deleteLatest;
public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) { public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) {
super(key, startTs, endTs); super(key, startTs, endTs);
this.rewriteLatestIfDeleted = rewriteLatestIfDeleted; this.rewriteLatestIfDeleted = rewriteLatestIfDeleted;
this.deleteLatest = deleteLatest;
}
public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) {
this(key, startTs, endTs, rewriteLatestIfDeleted, true);
} }
public BaseDeleteTsKvQuery(String key, long startTs, long endTs) { public BaseDeleteTsKvQuery(String key, long startTs, long endTs) {
this(key, startTs, endTs, false); this(key, startTs, endTs, false, true);
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/kv/DeleteTsKvQuery.java

@ -19,4 +19,6 @@ public interface DeleteTsKvQuery extends TsKvQuery {
Boolean getRewriteLatestIfDeleted(); Boolean getRewriteLatestIfDeleted();
Boolean getDeleteLatest();
} }

41
common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java

@ -16,11 +16,10 @@
package org.thingsboard.server.common.data.msg; package org.thingsboard.server.common.data.msg;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public enum TbMsgType { public enum TbMsgType {
@ -39,10 +38,10 @@ public enum TbMsgType {
ENTITY_UNASSIGNED("Entity Unassigned"), ENTITY_UNASSIGNED("Entity Unassigned"),
ATTRIBUTES_UPDATED("Attributes Updated"), ATTRIBUTES_UPDATED("Attributes Updated"),
ATTRIBUTES_DELETED("Attributes Deleted"), ATTRIBUTES_DELETED("Attributes Deleted"),
ALARM(null), ALARM,
ALARM_ACK("Alarm Acknowledged"), ALARM_ACK("Alarm Acknowledged"),
ALARM_CLEAR("Alarm Cleared"), ALARM_CLEAR("Alarm Cleared"),
ALARM_DELETE(null), ALARM_DELETE,
ALARM_ASSIGNED("Alarm Assigned"), ALARM_ASSIGNED("Alarm Assigned"),
ALARM_UNASSIGNED("Alarm Unassigned"), ALARM_UNASSIGNED("Alarm Unassigned"),
COMMENT_CREATED("Comment Created"), COMMENT_CREATED("Comment Created"),
@ -50,8 +49,8 @@ public enum TbMsgType {
RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"), RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"),
ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"), ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"),
ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"), ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"),
ENTITY_ASSIGNED_TO_EDGE(null), ENTITY_ASSIGNED_TO_EDGE,
ENTITY_UNASSIGNED_FROM_EDGE(null), ENTITY_UNASSIGNED_FROM_EDGE,
TIMESERIES_UPDATED("Timeseries Updated"), TIMESERIES_UPDATED("Timeseries Updated"),
TIMESERIES_DELETED("Timeseries Deleted"), TIMESERIES_DELETED("Timeseries Deleted"),
RPC_QUEUED("RPC Queued"), RPC_QUEUED("RPC Queued"),
@ -65,9 +64,9 @@ public enum TbMsgType {
RELATION_ADD_OR_UPDATE("Relation Added or Updated"), RELATION_ADD_OR_UPDATE("Relation Added or Updated"),
RELATION_DELETED("Relation Deleted"), RELATION_DELETED("Relation Deleted"),
RELATIONS_DELETED("All Relations Deleted"), RELATIONS_DELETED("All Relations Deleted"),
PROVISION_SUCCESS(null), PROVISION_SUCCESS,
PROVISION_FAILURE(null), PROVISION_FAILURE,
SEND_EMAIL(null), SEND_EMAIL,
// tellSelfOnly types // tellSelfOnly types
GENERATOR_NODE_SELF_MSG(null, true), GENERATOR_NODE_SELF_MSG(null, true),
@ -76,12 +75,15 @@ public enum TbMsgType {
DEVICE_UPDATE_SELF_MSG(null, true), DEVICE_UPDATE_SELF_MSG(null, true),
DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), DEDUPLICATION_TIMEOUT_SELF_MSG(null, true),
DELAY_TIMEOUT_SELF_MSG(null, true), DELAY_TIMEOUT_SELF_MSG(null, true),
MSG_COUNT_SELF_MSG(null, true); MSG_COUNT_SELF_MSG(null, true),
// Custom or N/A type:
NA;
public static final List<String> NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream() public static final List<String> NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream()
.filter(tbMsgType -> !tbMsgType.isTellSelfOnly()) .filter(tbMsgType -> !tbMsgType.isTellSelfOnly())
.map(TbMsgType::getRuleNodeConnection) .map(TbMsgType::getRuleNodeConnection)
.filter(Objects::nonNull) .filter(connection -> !TbNodeConnectionType.OTHER.equals(connection))
.collect(Collectors.toUnmodifiableList()); .collect(Collectors.toUnmodifiableList());
@Getter @Getter
@ -91,25 +93,16 @@ public enum TbMsgType {
private final boolean tellSelfOnly; private final boolean tellSelfOnly;
TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) { TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) {
this.ruleNodeConnection = ruleNodeConnection; this.ruleNodeConnection = StringUtils.isNotEmpty(ruleNodeConnection) ? ruleNodeConnection : TbNodeConnectionType.OTHER;
this.tellSelfOnly = tellSelfOnly; this.tellSelfOnly = tellSelfOnly;
} }
TbMsgType(String ruleNodeConnection) { TbMsgType(String ruleNodeConnection) {
this.ruleNodeConnection = ruleNodeConnection; this(ruleNodeConnection, false);
this.tellSelfOnly = false;
} }
public static String getRuleNodeConnectionOrElseOther(String msgType) { TbMsgType() {
if (msgType == null) { this(null, false);
return TbNodeConnectionType.OTHER;
} else {
return Arrays.stream(TbMsgType.values())
.filter(type -> type.name().equals(msgType))
.findFirst()
.map(TbMsgType::getRuleNodeConnection)
.orElse(TbNodeConnectionType.OTHER);
}
} }
} }

18
common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java

@ -22,6 +22,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE;
import static org.thingsboard.server.common.data.msg.TbMsgType.NA;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE;
@ -51,37 +52,36 @@ class TbMsgTypeTest {
DEVICE_UPDATE_SELF_MSG, DEVICE_UPDATE_SELF_MSG,
DEDUPLICATION_TIMEOUT_SELF_MSG, DEDUPLICATION_TIMEOUT_SELF_MSG,
DELAY_TIMEOUT_SELF_MSG, DELAY_TIMEOUT_SELF_MSG,
MSG_COUNT_SELF_MSG MSG_COUNT_SELF_MSG,
NA
); );
// backward-compatibility tests // backward-compatibility tests
@Test @Test
void getRuleNodeConnectionsTest() { void getRuleNodeConnectionsTest() {
var tbMsgTypes = TbMsgType.values(); var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) { for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) { if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(type.getRuleNodeConnection()).isNull(); assertThat(type.getRuleNodeConnection()).isEqualTo(TbNodeConnectionType.OTHER);
} else { } else {
assertThat(type.getRuleNodeConnection()).isNotNull(); assertThat(type.getRuleNodeConnection()).isNotEqualTo(TbNodeConnectionType.OTHER);
} }
} }
} }
@Test @Test
void getRuleNodeConnectionOrElseOtherTest() { void getRuleNodeConnectionOrElseOtherTest() {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(null))
.isEqualTo(TbNodeConnectionType.OTHER);
var tbMsgTypes = TbMsgType.values(); var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) { for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) { if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())) assertThat(type.getRuleNodeConnection())
.isEqualTo(TbNodeConnectionType.OTHER); .isEqualTo(TbNodeConnectionType.OTHER);
} else { } else {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())).isNotNull() assertThat(type.getRuleNodeConnection()).isNotNull()
.isNotEqualTo(TbNodeConnectionType.OTHER); .isNotEqualTo(TbNodeConnectionType.OTHER);
} }
} }
} }
} }

73
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -52,6 +52,7 @@ public final class TbMsg implements Serializable {
private final UUID id; private final UUID id;
private final long ts; private final long ts;
private final String type; private final String type;
private final TbMsgType internalType;
private final EntityId originator; private final EntityId originator;
private final CustomerId customerId; private final CustomerId customerId;
private final TbMsgMetaData metaData; private final TbMsgMetaData metaData;
@ -97,7 +98,7 @@ public final class TbMsg implements Serializable {
*/ */
@Deprecated(since = "3.5.2") @Deprecated(since = "3.5.2")
public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
} }
@ -108,7 +109,7 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.5.2", forRemoval = true) @Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
} }
@ -117,7 +118,7 @@ public final class TbMsg implements Serializable {
} }
public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
} }
@ -126,7 +127,7 @@ public final class TbMsg implements Serializable {
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
} }
@ -170,13 +171,13 @@ public final class TbMsg implements Serializable {
*/ */
@Deprecated(since = "3.5.2") @Deprecated(since = "3.5.2")
public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
} }
@Deprecated(since = "3.5.2", forRemoval = true) @Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY);
} }
@ -205,12 +206,12 @@ public final class TbMsg implements Serializable {
} }
public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY);
} }
@ -222,13 +223,13 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.5.2", forRemoval = true) @Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null,
metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
} }
@Deprecated(since = "3.5.2", forRemoval = true) @Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback);
} }
@ -250,72 +251,77 @@ public final class TbMsg implements Serializable {
*/ */
@Deprecated(since = "3.5.2") @Deprecated(since = "3.5.2")
public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback);
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
} }
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback);
} }
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback);
} }
public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgData(TbMsg tbMsg, String data) { public static TbMsg transformMsgData(TbMsg tbMsg, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback());
} }
//used for enqueueForTellNext //used for enqueueForTellNext
public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(),
tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY);
} }
private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this(queueName, id, ts, internalType, internalType.name(), originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, ctx, callback);
}
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this.id = id; this.id = id;
this.queueName = queueName; this.queueName = queueName;
@ -325,6 +331,7 @@ public final class TbMsg implements Serializable {
this.ts = System.currentTimeMillis(); this.ts = System.currentTimeMillis();
} }
this.type = type; this.type = type;
this.internalType = internalType != null ? internalType : getInternalType(type);
this.originator = originator; this.originator = originator;
if (customerId == null || customerId.isNullUid()) { if (customerId == null || customerId.isNullUid()) {
if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) {
@ -410,7 +417,7 @@ public final class TbMsg implements Serializable {
} }
TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId,
metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback);
} catch (InvalidProtocolBufferException e) { } catch (InvalidProtocolBufferException e) {
throw new IllegalStateException("Could not parse protobuf for TbMsg", e); throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
@ -422,17 +429,17 @@ public final class TbMsg implements Serializable {
} }
public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback);
} }
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback);
} }
public TbMsg copyWithNewCtx() { public TbMsg copyWithNewCtx() {
return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId, return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY);
} }
@ -468,8 +475,16 @@ public final class TbMsg implements Serializable {
return ts; return ts;
} }
private TbMsgType getInternalType(String type) {
try {
return TbMsgType.valueOf(type);
} catch (IllegalArgumentException e) {
return TbMsgType.NA;
}
}
public boolean isTypeOf(TbMsgType tbMsgType) { public boolean isTypeOf(TbMsgType tbMsgType) {
return tbMsgType != null && tbMsgType.name().equals(this.type); return internalType.equals(tbMsgType);
} }
public boolean isTypeOneOf(TbMsgType... types) { public boolean isTypeOneOf(TbMsgType... types) {

5
common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java

@ -32,6 +32,11 @@ public class RuleEngineException extends Exception {
ts = System.currentTimeMillis(); ts = System.currentTimeMillis();
} }
public RuleEngineException(String message, Throwable t) {
super(message != null ? message : "Unknown", t);
ts = System.currentTimeMillis();
}
public String toJsonString() { public String toJsonString() {
try { try {
return mapper.writeValueAsString(mapper.createObjectNode().put("message", getMessage())); return mapper.writeValueAsString(mapper.createObjectNode().put("message", getMessage()));

4
common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java

@ -39,6 +39,10 @@ public interface TbMsgCallback {
void onFailure(RuleEngineException e); void onFailure(RuleEngineException e);
default void onRateLimit(RuleEngineException e) {
onFailure(e);
};
/** /**
* Returns 'true' if rule engine is expecting the message to be processed, 'false' otherwise. * Returns 'true' if rule engine is expecting the message to be processed, 'false' otherwise.
* message may no longer be valid, if the message pack is already expired/canceled/failed. * message may no longer be valid, if the message pack is already expired/canceled/failed.

3
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java

@ -17,11 +17,12 @@ package org.thingsboard.server.common.msg.tools;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.AbstractRateLimitException;
/** /**
* Created by ashvayka on 22.10.18. * Created by ashvayka on 22.10.18.
*/ */
public class TbRateLimitsException extends RuntimeException { public class TbRateLimitsException extends AbstractRateLimitException {
@Getter @Getter
private final EntityType entityType; private final EntityType entityType;

2
common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueCallbackWrapper.java

@ -40,6 +40,6 @@ public class MultipleTbQueueCallbackWrapper implements TbQueueCallback {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
callback.onFailure(new RuleEngineException(t.getMessage())); callback.onFailure(new RuleEngineException(t.getMessage(), t));
} }
} }

2
common/queue/src/main/java/org/thingsboard/server/queue/common/MultipleTbQueueTbMsgCallbackWrapper.java

@ -41,6 +41,6 @@ public class MultipleTbQueueTbMsgCallbackWrapper implements TbQueueCallback {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
tbMsgCallback.onFailure(new RuleEngineException(t.getMessage())); tbMsgCallback.onFailure(new RuleEngineException(t.getMessage(), t));
} }
} }

2
common/queue/src/main/java/org/thingsboard/server/queue/common/TbQueueTbMsgCallbackWrapper.java

@ -35,6 +35,6 @@ public class TbQueueTbMsgCallbackWrapper implements TbQueueCallback {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
tbMsgCallback.onFailure(new RuleEngineException(t.getMessage())); tbMsgCallback.onFailure(new RuleEngineException(t.getMessage(), t));
} }
} }

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java

@ -69,7 +69,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
private Integer zkSessionTimeout; private Integer zkSessionTimeout;
@Value("${zk.zk_dir}") @Value("${zk.zk_dir}")
private String zkDir; private String zkDir;
@Value("${zk.recalculate_delay:60000}") @Value("${zk.recalculate_delay:0}")
private Long recalculateDelay; private Long recalculateDelay;
protected final ConcurrentHashMap<String, ScheduledFuture<?>> delayedTasks; protected final ConcurrentHashMap<String, ScheduledFuture<?>> delayedTasks;

28
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java

@ -30,6 +30,9 @@ import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -53,10 +56,14 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
private final Set<TopicPartitionInfo> topics; private final Set<TopicPartitionInfo> topics;
@Getter
private final String clientId;
@Builder @Builder
private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) { private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) {
Properties props = settings.toProducerProps(); Properties props = settings.toProducerProps();
this.clientId = Objects.requireNonNull(clientId, "Kafka producer client.id is null");
if (!StringUtils.isEmpty(clientId)) { if (!StringUtils.isEmpty(clientId)) {
props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
} }
@ -72,6 +79,22 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
public void init() { public void init() {
} }
void addAnalyticHeaders(List<Header> headers) {
headers.add(new RecordHeader("_producerId", getClientId().getBytes(StandardCharsets.UTF_8)));
headers.add(new RecordHeader("_threadName", Thread.currentThread().getName().getBytes(StandardCharsets.UTF_8)));
if (log.isTraceEnabled()) {
try {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
int maxlevel = Math.min(stackTrace.length, 10);
for (int i = 2; i < maxlevel; i++) { // ignore two levels: getStackTrace and addAnalyticHeaders
headers.add(new RecordHeader("_stackTrace" + i, stackTrace[i].toString().getBytes(StandardCharsets.UTF_8)));
}
} catch (Throwable t) {
log.trace("Failed to add stacktrace headers in Kafka producer {}", getClientId(), t);
}
}
}
@Override @Override
public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) {
try { try {
@ -79,7 +102,10 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
String key = msg.getKey().toString(); String key = msg.getKey().toString();
byte[] data = msg.getData(); byte[] data = msg.getData();
ProducerRecord<String, byte[]> record; ProducerRecord<String, byte[]> record;
Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); List<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList());
if (log.isDebugEnabled()) {
addAnalyticHeaders(headers);
}
record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers);
producer.send(record, (metadata, exception) -> { producer.send(record, (metadata, exception) -> {
if (exception == null) { if (exception == null) {

54
common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplateTest.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2023 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.queue.kafka;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.common.header.Header;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.queue.TbQueueMsg;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
@Slf4j
class TbKafkaProducerTemplateTest {
TbKafkaProducerTemplate<TbQueueMsg> producerTemplate;
@BeforeEach
void setUp() {
producerTemplate = mock(TbKafkaProducerTemplate.class);
willCallRealMethod().given(producerTemplate).addAnalyticHeaders(any());
willReturn("tb-core-to-core-notifications-tb-core-3").given(producerTemplate).getClientId();
}
@Test
void testAddAnalyticHeaders() {
List<Header> headers = new ArrayList<>();
producerTemplate.addAnalyticHeaders(headers);
assertThat(headers).isNotEmpty();
headers.forEach(r -> log.info("RecordHeader key [{}] value [{}]", r.key(), new String(r.value(), StandardCharsets.UTF_8)));
}
}

20
common/queue/src/test/resources/logback-test.xml

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- TbKafkaProducerTemplate will add headers for each message when log level:
- DEBUG - producerId and thread name
- TRACE - will add stacktrace.
Kafka compression is highly recommended -->
<logger name="org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate" level="TRACE"/>
<root level="INFO">
<appender-ref ref="console"/>
</root>
</configuration>

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

@ -57,6 +57,8 @@ public class TbUtils {
List.class))); List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson", parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, List.class))); ExecutionContext.class, List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
@ -174,6 +176,9 @@ public class TbUtils {
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException { public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList)); return TbJson.parse(ctx, bytesToString(bytesList));
} }
public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException {
return TbJson.parse(ctx, jsonStr);
}
public static String bytesToString(List<Byte> bytesList) { public static String bytesToString(List<Byte> bytesList) {
byte[] bytes = bytesFromList(bytesList); byte[] bytes = bytesFromList(bytesList);

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

@ -27,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap; import org.mvel2.execution.ExecutionHashMap;
import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@ -348,6 +349,24 @@ public class TbUtilsTest {
Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false))); Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
} }
@Test
public void parseBytesDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assert.assertEquals(expectedJson,actualJson);
}
@Test
public void parseStringDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assert.assertEquals(expectedJson,actualJson);
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

11
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -30,6 +30,7 @@ import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.FeatureType;
@ -379,12 +380,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
} }
} }
private Optional<FeatureType> getFeatureType(Request request) { protected Optional<FeatureType> getFeatureType(Request request) {
List<String> uriPath = request.getOptions().getUriPath(); List<String> uriPath = request.getOptions().getUriPath();
try { try {
if (uriPath.size() >= FEATURE_TYPE_POSITION) { int size = uriPath.size();
if (size >= FEATURE_TYPE_POSITION) {
if (size == FEATURE_TYPE_POSITION && StringUtils.isNumeric(uriPath.get(size - 1))) {
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 2).toUpperCase()));
}
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
} else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { } else if (size == FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) {
if (uriPath.contains(DataConstants.PROVISION)) { if (uriPath.contains(DataConstants.PROVISION)) {
return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
} }

161
common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapTransportResourceTest.java

@ -0,0 +1,161 @@
/**
* Copyright © 2016-2023 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.transport.coap;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.OptionSet;
import org.eclipse.californium.core.coap.Request;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.thingsboard.server.coapserver.CoapServerService;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.transport.coap.client.CoapClientContext;
import java.util.Random;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class CoapTransportResourceTest {
private static final String V1 = "v1";
private static final String API = "api";
private static final String TELEMETRY = "telemetry";
private static final String ATTRIBUTES = "attributes";
private static final String RPC = "rpc";
private static final String CLAIM = "claim";
private static final String PROVISION = "provision";
private static final String GET_ATTRIBUTES_URI_QUERY = "clientKeys=attribute1,attribute2&sharedKeys=shared1,shared2";
private static final Random RANDOM = new Random();
private static CoapTransportResource coapTransportResource;
@BeforeAll
static void setUp() {
var ctxMock = mock(CoapTransportContext.class);
var coapServerServiceMock = mock(CoapServerService.class);
var transportServiceMock = mock(TransportService.class);
var clientContextMock = mock(CoapClientContext.class);
var schedulerComponentMock = mock(SchedulerComponent.class);
when(ctxMock.getTransportService()).thenReturn(transportServiceMock);
when(ctxMock.getClientContext()).thenReturn(clientContextMock);
when(ctxMock.getSessionReportTimeout()).thenReturn(1L);
when(ctxMock.getScheduler()).thenReturn(schedulerComponentMock);
coapTransportResource = new CoapTransportResource(ctxMock, coapServerServiceMock, V1);
}
@ParameterizedTest
@MethodSource("provideRequestAndFeatureType")
void givenRequest_whenGetFeatureType_thenReturnedExpectedFeatureType(Request request, FeatureType expectedFeatureType) {
var featureTypeOptional = coapTransportResource.getFeatureType(request);
assertTrue(featureTypeOptional.isPresent(), "Optional<FeatureType> is empty");
assertEquals(expectedFeatureType, featureTypeOptional.get(), "Feature type is invalid");
}
static Stream<Arguments> provideRequestAndFeatureType() {
return Stream.of(
// accessToken based tests
Arguments.of(toAccessTokenRequest(CoAP.Code.POST, TELEMETRY), FeatureType.TELEMETRY),
Arguments.of(toAccessTokenRequest(CoAP.Code.POST, ATTRIBUTES), FeatureType.ATTRIBUTES),
Arguments.of(toGetAttributesAccessTokenRequest(), FeatureType.ATTRIBUTES),
Arguments.of(toAccessTokenRequest(CoAP.Code.GET, ATTRIBUTES), FeatureType.ATTRIBUTES),
Arguments.of(toAccessTokenRequest(CoAP.Code.GET, RPC), FeatureType.RPC),
Arguments.of(toRpcResponseAccessTokenRequest(), FeatureType.RPC),
Arguments.of(toAccessTokenRequest(CoAP.Code.POST, RPC), FeatureType.RPC),
Arguments.of(toAccessTokenRequest(CoAP.Code.POST, CLAIM), FeatureType.CLAIM),
// certificate based tests
Arguments.of(toCertificateRequest(CoAP.Code.POST, TELEMETRY), FeatureType.TELEMETRY),
Arguments.of(toCertificateRequest(CoAP.Code.POST, ATTRIBUTES), FeatureType.ATTRIBUTES),
Arguments.of(toGetAttributesCertificateRequest(), FeatureType.ATTRIBUTES),
Arguments.of(toCertificateRequest(CoAP.Code.GET, ATTRIBUTES), FeatureType.ATTRIBUTES),
Arguments.of(toCertificateRequest(CoAP.Code.GET, RPC), FeatureType.RPC),
Arguments.of(toRpcResponseCertificateRequest(), FeatureType.RPC),
Arguments.of(toCertificateRequest(CoAP.Code.POST, RPC), FeatureType.RPC),
Arguments.of(toCertificateRequest(CoAP.Code.POST, CLAIM), FeatureType.CLAIM),
// provision request
Arguments.of(toProvisionRequest(), FeatureType.PROVISION)
);
}
private static Request toAccessTokenRequest(CoAP.Code method, String featureType) {
return getAccessTokenRequest(method, featureType, null, null);
}
private static Request toGetAttributesAccessTokenRequest() {
return getAccessTokenRequest(CoAP.Code.GET, CoapTransportResourceTest.ATTRIBUTES, null, CoapTransportResourceTest.GET_ATTRIBUTES_URI_QUERY);
}
private static Request toRpcResponseAccessTokenRequest() {
return getAccessTokenRequest(CoAP.Code.POST, CoapTransportResourceTest.RPC, RANDOM.nextInt(100), null);
}
private static Request toCertificateRequest(CoAP.Code method, String featureType) {
return getCertificateRequest(method, featureType, null, null);
}
private static Request toGetAttributesCertificateRequest() {
return getCertificateRequest(CoAP.Code.GET, CoapTransportResourceTest.ATTRIBUTES, null, CoapTransportResourceTest.GET_ATTRIBUTES_URI_QUERY);
}
private static Request toRpcResponseCertificateRequest() {
return getCertificateRequest(CoAP.Code.POST, CoapTransportResourceTest.RPC, RANDOM.nextInt(100), null);
}
private static Request getAccessTokenRequest(CoAP.Code method, String featureType, Integer requestId, String uriQuery) {
return getRequest(method, featureType, false, requestId, uriQuery);
}
private static Request getCertificateRequest(CoAP.Code method, String featureType, Integer requestId, String uriQuery) {
return getRequest(method, featureType, true, requestId, uriQuery);
}
private static Request toProvisionRequest() {
return getRequest(CoAP.Code.POST, PROVISION, true, null, null);
}
private static Request getRequest(CoAP.Code method, String featureType, boolean dtls, Integer requestId, String uriQuery) {
var request = new Request(method);
var options = new OptionSet();
options.addUriPath(API);
options.addUriPath(V1);
if (!dtls) {
options.addUriPath(StringUtils.randomAlphanumeric(20));
}
options.addUriPath(featureType);
if (requestId != null) {
options.addUriPath(String.valueOf(requestId));
}
if (uriQuery != null) {
options.setUriQuery(uriQuery);
}
request.setOptions(options);
return request;
}
}

67
common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2023 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.common.util;
import com.google.gson.JsonParseException;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import javax.script.ScriptException;
import java.io.PrintWriter;
import java.io.StringWriter;
@Slf4j
public class ExceptionUtil {
@SuppressWarnings("unchecked")
public static <T extends Exception> T lookupException(Throwable source, Class<T> clazz) {
Exception e = lookupExceptionInCause(source, clazz);
if (e != null) {
return (T) e;
} else {
return null;
}
}
public static Exception lookupExceptionInCause(Throwable source, Class<? extends Exception>... clazzes) {
while (source != null) {
for (Class<? extends Exception> clazz : clazzes) {
if (clazz.isAssignableFrom(source.getClass())) {
return (Exception) source;
}
}
source = source.getCause();
}
return null;
}
public static String toString(Exception e, EntityId componentId, boolean stackTraceEnabled) {
Exception exception = lookupExceptionInCause(e, ScriptException.class, JsonParseException.class);
if (exception != null && StringUtils.isNotEmpty(exception.getMessage())) {
return exception.getMessage();
} else {
if (stackTraceEnabled) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return sw.toString();
} else {
log.debug("[{}] Unknown error during message processing", componentId, e);
return "Please contact system administrator";
}
}
}
}

74
common/util/src/test/java/org/thingsboard/common/util/ExceptionUtilTest.java

@ -0,0 +1,74 @@
/**
* Copyright © 2016-2023 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.common.util;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
class ExceptionUtilTest {
final Exception cause = new RuntimeException();
@Test
void givenRootCause_whenLookupExceptionInCause_thenReturnRootCauseAndNoStackOverflow() {
Exception e = cause;
for (int i = 0; i <= 16384; i++) {
e = new Exception(e);
}
assertThat(ExceptionUtil.lookupExceptionInCause(e, RuntimeException.class)).isSameAs(cause);
}
@Test
void givenCause_whenLookupExceptionInCause_thenReturnCause() {
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(cause), RuntimeException.class)).isSameAs(cause);
}
@Test
void givenNoCauseAndExceptionIsWantedCauseClass_whenLookupExceptionInCause_thenReturnSelf() {
assertThat(ExceptionUtil.lookupExceptionInCause(cause, RuntimeException.class)).isSameAs(cause);
}
@Test
void givenNoCause_whenLookupExceptionInCause_thenReturnNull() {
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(), RuntimeException.class)).isNull();
}
@Test
void givenNotWantedCause_whenLookupExceptionInCause_thenReturnNull() {
final Exception cause = new IOException();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(cause), RuntimeException.class)).isNull();
}
@Test
void givenCause_whenLookupExceptionInCauseByMany_thenReturnFirstCause() {
final Exception causeIAE = new IllegalAccessException();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE))).isNull();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IOException.class, NoSuchFieldException.class)).isNull();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IllegalAccessException.class, IOException.class, NoSuchFieldException.class)).isSameAs(causeIAE);
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), IOException.class, NoSuchFieldException.class, IllegalAccessException.class)).isSameAs(causeIAE);
final Exception causeIOE = new IOException(causeIAE);
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE))).isNull();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIAE), ClassNotFoundException.class, NoSuchFieldException.class)).isNull();
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IOException.class, NoSuchFieldException.class)).isSameAs(causeIOE);
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IllegalAccessException.class, IOException.class, NoSuchFieldException.class)).isSameAs(causeIOE);
assertThat(ExceptionUtil.lookupExceptionInCause(new Exception(causeIOE), IOException.class, NoSuchFieldException.class, IllegalAccessException.class)).isSameAs(causeIOE);
}
}

3
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -81,6 +81,9 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), recipientId.getId(), status) != 0; return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), recipientId.getId(), status) != 0;
} }
/**
* For this hot method, the partial index `idx_notification_recipient_id_unread` was introduced since 3.5.2
* */
@Override @Override
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) { public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ); return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ);

4
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -275,7 +275,9 @@ public class BaseTimeseriesService implements TimeseriesService {
private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) { private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) {
futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor())); futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); if (query.getDeleteLatest()) {
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
}
} }
private static void validate(EntityId entityId) { private static void validate(EntityId entityId) {

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -113,3 +113,5 @@ CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_reque
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id); CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';

25
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -28,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
@ -74,8 +76,18 @@ import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevi
public class MqttClientTest extends AbstractContainerTest { public class MqttClientTest extends AbstractContainerTest {
private Device device; private Device device;
AbstractListeningExecutor handlerExecutor;
@BeforeMethod @BeforeMethod
public void setUp() throws Exception { public void setUp() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
testRestClient.login("tenant@thingsboard.org", "tenant"); testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_")); device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
} }
@ -83,6 +95,9 @@ public class MqttClientTest extends AbstractContainerTest {
@AfterMethod @AfterMethod
public void tearDown() { public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId()); testRestClient.deleteDeviceIfExists(device.getId());
if (handlerExecutor != null) {
handlerExecutor.destroy();
}
} }
@Test @Test
public void telemetryUpload() throws Exception { public void telemetryUpload() throws Exception {
@ -461,11 +476,16 @@ public class MqttClientTest extends AbstractContainerTest {
return getMqttClient(deviceCredentials.getCredentialsId(), listener); return getMqttClient(deviceCredentials.getCredentialsId(), listener);
} }
private String getOwnerId() {
return "Tenant[" + device.getTenantId().getId() + "]MqttClientTestDevice[" + device.getId().getId() + "]";
}
private MqttClient getMqttClient(String username, MqttMessageListener listener) throws InterruptedException, ExecutionException { private MqttClient getMqttClient(String username, MqttMessageListener listener) throws InterruptedException, ExecutionException {
MqttClientConfig clientConfig = new MqttClientConfig(); MqttClientConfig clientConfig = new MqttClientConfig();
clientConfig.setOwnerId(getOwnerId());
clientConfig.setClientId("MQTT client from test"); clientConfig.setClientId("MQTT client from test");
clientConfig.setUsername(username); clientConfig.setUsername(username);
MqttClient mqttClient = MqttClient.create(clientConfig, listener); MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor);
mqttClient.connect("localhost", 1883).get(); mqttClient.connect("localhost", 1883).get();
return mqttClient; return mqttClient;
} }
@ -479,9 +499,10 @@ public class MqttClientTest extends AbstractContainerTest {
} }
@Override @Override
public void onMessage(String topic, ByteBuf message) { public ListenableFuture<Void> onMessage(String topic, ByteBuf message) {
log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic); log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8))); events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
return Futures.immediateVoidFuture();
} }
} }

25
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.msa.connectivity; package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -32,6 +33,7 @@ import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
@ -76,8 +78,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
private MqttMessageListener listener; private MqttMessageListener listener;
private JsonParser jsonParser = new JsonParser(); private JsonParser jsonParser = new JsonParser();
AbstractListeningExecutor handlerExecutor;
@BeforeMethod @BeforeMethod
public void createGateway() throws Exception { public void createGateway() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
testRestClient.login("tenant@thingsboard.org", "tenant"); testRestClient.login("tenant@thingsboard.org", "tenant");
gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype()); gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype());
DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId()); DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId());
@ -94,6 +106,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
this.listener = null; this.listener = null;
this.mqttClient = null; this.mqttClient = null;
this.createdDevice = null; this.createdDevice = null;
if (handlerExecutor != null) {
handlerExecutor.destroy();
}
} }
@Test @Test
@ -403,11 +418,16 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
return testRestClient.getDeviceById(createdDeviceId); return testRestClient.getDeviceById(createdDeviceId);
} }
private String getOwnerId() {
return "Tenant[" + gatewayDevice.getTenantId().getId() + "]MqttGatewayClientTestDevice[" + gatewayDevice.getId().getId() + "]";
}
private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException { private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException {
MqttClientConfig clientConfig = new MqttClientConfig(); MqttClientConfig clientConfig = new MqttClientConfig();
clientConfig.setOwnerId(getOwnerId());
clientConfig.setClientId("MQTT client from test"); clientConfig.setClientId("MQTT client from test");
clientConfig.setUsername(deviceCredentials.getCredentialsId()); clientConfig.setUsername(deviceCredentials.getCredentialsId());
MqttClient mqttClient = MqttClient.create(clientConfig, listener); MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor);
mqttClient.connect("localhost", 1883).get(); mqttClient.connect("localhost", 1883).get();
return mqttClient; return mqttClient;
} }
@ -421,9 +441,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
} }
@Override @Override
public void onMessage(String topic, ByteBuf message) { public ListenableFuture<Void> onMessage(String topic, ByteBuf message) {
log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic); log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8))); events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
return Futures.immediateVoidFuture();
} }
} }

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

@ -41,7 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)

4
netty-mqtt/pom.xml

@ -35,6 +35,10 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.netty</groupId> <groupId>io.netty</groupId>
<artifactId>netty-codec-mqtt</artifactId> <artifactId>netty-codec-mqtt</artifactId>

108
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java

@ -16,6 +16,10 @@
package org.thingsboard.mqtt; package org.thingsboard.mqtt;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
@ -34,9 +38,13 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckMessage;
import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j @Slf4j
final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage> { final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage> {
@ -117,27 +125,48 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
super.channelInactive(ctx); super.channelInactive(ctx);
} }
private void invokeHandlersForIncomingPublish(MqttPublishMessage message) { ListenableFuture<Void> invokeHandlersForIncomingPublish(MqttPublishMessage message) {
boolean handlerInvoked = false; var future = Futures.immediateVoidFuture();
for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { var handlerInvoked = new AtomicBoolean();
if (subscription.matches(message.variableHeader().topicName())) { try {
if (subscription.isOnce() && subscription.isCalled()) { for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) {
continue; if (subscription.matches(message.variableHeader().topicName())) {
} future = Futures.transform(future, x -> {
message.payload().markReaderIndex(); if (subscription.isOnce() && subscription.isCalled()) {
subscription.setCalled(true); return null;
subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); }
if (subscription.isOnce()) { message.payload().markReaderIndex();
this.client.off(subscription.getTopic(), subscription.getHandler()); subscription.setCalled(true);
subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
if (subscription.isOnce()) {
this.client.off(subscription.getTopic(), subscription.getHandler());
}
message.payload().resetReaderIndex();
handlerInvoked.set(true);
return null;
}, client.getHandlerExecutor());
} }
message.payload().resetReaderIndex();
handlerInvoked = true;
} }
future = Futures.transform(future, x -> {
if (!handlerInvoked.get() && client.getDefaultHandler() != null) {
client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload());
}
return null;
}, client.getHandlerExecutor());
} finally {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
message.payload().release();
}
@Override
public void onFailure(Throwable t) {
message.payload().release();
}
}, MoreExecutors.directExecutor());
} }
if (!handlerInvoked && client.getDefaultHandler() != null) { return future;
client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload());
}
message.payload().release();
} }
private void handleConack(Channel channel, MqttConnAckMessage message) { private void handleConack(Channel channel, MqttConnAckMessage message) {
@ -204,11 +233,13 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
break; break;
case AT_LEAST_ONCE: case AT_LEAST_ONCE:
invokeHandlersForIncomingPublish(message); var future = invokeHandlersForIncomingPublish(message);
if (message.variableHeader().packetId() != -1) { if (message.variableHeader().packetId() != -1) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); future.addListener(() -> {
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId()); MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader)); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader));
}, MoreExecutors.directExecutor());
} }
break; break;
@ -263,14 +294,20 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
} }
private void handlePubrel(Channel channel, MqttMessage message) { private void handlePubrel(Channel channel, MqttMessage message) {
var future = Futures.immediateVoidFuture();
if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) { if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) {
MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); future = invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId()); future = Futures.transform(future, x -> {
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId());
return null;
}, MoreExecutors.directExecutor());
} }
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); future.addListener(() -> {
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader)); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader));
}, MoreExecutors.directExecutor());
} }
private void handlePubcomp(MqttMessage message) { private void handlePubcomp(MqttMessage message) {
@ -281,4 +318,21 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
pendingPublish.getPayload().release(); pendingPublish.getPayload().release();
pendingPublish.onPubcompReceived(); pendingPublish.onPubcompReceived();
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
try {
if (cause instanceof IOException) {
if (log.isDebugEnabled()) {
log.debug("[{}] IOException: ", client.getClientConfig().getOwnerId(), cause);
} else {
log.info("[{}] IOException: {}", client.getClientConfig().getOwnerId(), cause.getMessage());
}
} else {
log.warn("[{}] exceptionCaught", client.getClientConfig().getOwnerId(), cause);
}
} finally {
ReferenceCountUtil.release(cause);
}
}
} }

7
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java

@ -21,6 +21,7 @@ import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import org.thingsboard.common.util.ListeningExecutor;
public interface MqttClient { public interface MqttClient {
@ -71,6 +72,8 @@ public interface MqttClient {
*/ */
void setEventLoop(EventLoopGroup eventLoop); void setEventLoop(EventLoopGroup eventLoop);
ListeningExecutor getHandlerExecutor();
/** /**
* Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* *
@ -180,8 +183,8 @@ public interface MqttClient {
* @param config The config object to use while looking for settings * @param config The config object to use while looking for settings
* @param defaultHandler The handler for incoming messages that do not match any topic subscriptions * @param defaultHandler The handler for incoming messages that do not match any topic subscriptions
*/ */
static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler){ static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler, ListeningExecutor handlerExecutor){
return new MqttClientImpl(config, defaultHandler); return new MqttClientImpl(config, defaultHandler, handlerExecutor);
} }
/** /**

6
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java

@ -19,6 +19,8 @@ import io.netty.channel.Channel;
import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -26,10 +28,12 @@ import java.util.Random;
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public final class MqttClientConfig { public final class MqttClientConfig {
private final SslContext sslContext; private final SslContext sslContext;
private final String randomClientId; private final String randomClientId;
@Getter
@Setter
private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes
private String clientId; private String clientId;
private int timeoutSeconds = 60; private int timeoutSeconds = 60;
private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; private MqttVersion protocolVersion = MqttVersion.MQTT_3_1;

15
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -46,6 +46,7 @@ import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ListeningExecutor;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -88,13 +89,13 @@ final class MqttClientImpl implements MqttClient {
private int port; private int port;
private MqttClientCallback callback; private MqttClientCallback callback;
private final ListeningExecutor handlerExecutor;
/** /**
* Construct the MqttClientImpl with default config * Construct the MqttClientImpl with default config
*/ */
public MqttClientImpl(MqttHandler defaultHandler) { public MqttClientImpl(MqttHandler defaultHandler, ListeningExecutor handlerExecutor) {
this.clientConfig = new MqttClientConfig(); this(new MqttClientConfig(), defaultHandler, handlerExecutor);
this.defaultHandler = defaultHandler;
} }
/** /**
@ -103,9 +104,10 @@ final class MqttClientImpl implements MqttClient {
* *
* @param clientConfig The config object to use while looking for settings * @param clientConfig The config object to use while looking for settings
*/ */
public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler) { public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler, ListeningExecutor handlerExecutor) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.defaultHandler = defaultHandler; this.defaultHandler = defaultHandler;
this.handlerExecutor = handlerExecutor;
} }
/** /**
@ -227,6 +229,11 @@ final class MqttClientImpl implements MqttClient {
this.eventLoop = eventLoop; this.eventLoop = eventLoop;
} }
@Override
public ListeningExecutor getHandlerExecutor() {
return this.handlerExecutor;
}
/** /**
* Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* *

3
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java

@ -15,9 +15,10 @@
*/ */
package org.thingsboard.mqtt; package org.thingsboard.mqtt;
import com.google.common.util.concurrent.ListenableFuture;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public interface MqttHandler { public interface MqttHandler {
void onMessage(String topic, ByteBuf payload); ListenableFuture<Void> onMessage(String topic, ByteBuf payload);
} }

2
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java

@ -25,7 +25,7 @@ final class MqttSubscription {
private final boolean once; private final boolean once;
private boolean called; private volatile boolean called;
MqttSubscription(String topic, MqttHandler handler, boolean once) { MqttSubscription(String topic, MqttHandler handler, boolean once) {
if (topic == null) { if (topic == null) {

17
netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java

@ -26,6 +26,7 @@ import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttConnectResult; import org.thingsboard.mqtt.MqttConnectResult;
@ -49,8 +50,18 @@ public class MqttIntegrationTest {
MqttClient mqttClient; MqttClient mqttClient;
AbstractListeningExecutor handlerExecutor;
@Before @Before
public void init() throws Exception { public void init() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
this.eventLoopGroup = new NioEventLoopGroup(); this.eventLoopGroup = new NioEventLoopGroup();
this.mqttServer = new MqttServer(); this.mqttServer = new MqttServer();
@ -68,6 +79,9 @@ public class MqttIntegrationTest {
if (this.eventLoopGroup != null) { if (this.eventLoopGroup != null) {
this.eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); this.eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
} }
if (this.handlerExecutor != null) {
this.handlerExecutor.destroy();
}
} }
@Test @Test
@ -108,9 +122,10 @@ public class MqttIntegrationTest {
private MqttClient initClient() throws Exception { private MqttClient initClient() throws Exception {
MqttClientConfig config = new MqttClientConfig(); MqttClientConfig config = new MqttClientConfig();
config.setOwnerId("MqttIntegrationTest");
config.setTimeoutSeconds(KEEPALIVE_TIMEOUT_SECONDS); config.setTimeoutSeconds(KEEPALIVE_TIMEOUT_SECONDS);
config.setReconnectDelay(RECONNECT_DELAY_SECONDS); config.setReconnectDelay(RECONNECT_DELAY_SECONDS);
MqttClient client = MqttClient.create(config, null); MqttClient client = MqttClient.create(config, null, handlerExecutor);
client.setEventLoop(this.eventLoopGroup); client.setEventLoop(this.eventLoopGroup);
Future<MqttConnectResult> connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort()); Future<MqttConnectResult> connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort());

22
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -2364,7 +2364,8 @@ public class RestClient implements Closeable {
boolean deleteAllDataForKeys, boolean deleteAllDataForKeys,
Long startTs, Long startTs,
Long endTs, Long endTs,
boolean rewriteLatestIfDeleted) { boolean rewriteLatestIfDeleted,
boolean deleteLatest) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name()); params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString()); params.put("entityId", entityId.getId().toString());
@ -2373,17 +2374,34 @@ public class RestClient implements Closeable {
params.put("startTs", startTs.toString()); params.put("startTs", startTs.toString());
params.put("endTs", endTs.toString()); params.put("endTs", endTs.toString());
params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted)); params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted));
params.put("deleteLatest", String.valueOf(deleteLatest));
return restTemplate return restTemplate
.exchange( .exchange(
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}", baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}&deleteLatest={deleteLatest}",
HttpMethod.DELETE, HttpMethod.DELETE,
HttpEntity.EMPTY, HttpEntity.EMPTY,
Object.class, Object.class,
params) params)
.getStatusCode() .getStatusCode()
.is2xxSuccessful(); .is2xxSuccessful();
}
public boolean deleteEntityLatestTimeseries(EntityId entityId, List<String> keys) {
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString());
params.put("keys", listToString(keys));
return restTemplate
.exchange(
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/latest/delete?keys={keys}",
HttpMethod.DELETE,
HttpEntity.EMPTY,
Object.class,
params)
.getStatusCode()
.is2xxSuccessful();
} }
public boolean deleteEntityAttributes(DeviceId deviceId, String scope, List<String> keys) { public boolean deleteEntityAttributes(DeviceId deviceId, String scope, List<String> keys) {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java

@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
@ -50,7 +49,7 @@ public class TbMsgTypeSwitchNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
ctx.tellNext(msg, TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); ctx.tellNext(msg, msg.getInternalType().getRuleNodeConnection());
} }
} }

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNodeConfiguration.java

@ -32,9 +32,10 @@ public class TbMathNodeConfiguration implements NodeConfiguration<TbMathNodeConf
@Override @Override
public TbMathNodeConfiguration defaultConfiguration() { public TbMathNodeConfiguration defaultConfiguration() {
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration(); TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
configuration.setOperation(TbRuleNodeMathFunctionType.ADD); configuration.setOperation(TbRuleNodeMathFunctionType.CUSTOM);
configuration.setArguments(Arrays.asList(new TbMathArgument("x", TbMathArgumentType.CONSTANT, "2"), new TbMathArgument("y", TbMathArgumentType.CONSTANT, "2"))); configuration.setCustomFunction("(t - 32) / 1.8");
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null)); configuration.setArguments(List.of(new TbMathArgument("t", TbMathArgumentType.MESSAGE_BODY, "temperature")));
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "temperatureCelsius", 2, false, false, null));
return configuration; return configuration;
} }
} }

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -105,8 +105,13 @@ public class TbMqttNode extends TbAbstractExternalNode {
} }
} }
String getOwnerId(TbContext ctx) {
return "Tenant[" + ctx.getTenantId().getId() + "]RuleNode[" + ctx.getSelf().getId().getId() + "]";
}
protected MqttClient initClient(TbContext ctx) throws Exception { protected MqttClient initClient(TbContext ctx) throws Exception {
MqttClientConfig config = new MqttClientConfig(getSslContext()); MqttClientConfig config = new MqttClientConfig(getSslContext());
config.setOwnerId(getOwnerId(ctx));
if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId());
@ -114,7 +119,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());
prepareMqttClientConfig(config); prepareMqttClientConfig(config);
MqttClient client = MqttClient.create(config, null); MqttClient client = MqttClient.create(config, null, ctx.getExternalCallExecutor());
client.setEventLoop(ctx.getSharedEventLoop()); client.setEventLoop(ctx.getSharedEventLoop());
Future<MqttConnectResult> connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); Future<MqttConnectResult> connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort());
MqttConnectResult result; MqttConnectResult result;

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java

@ -39,7 +39,7 @@ public class MultipleTbMsgsCallbackWrapper implements TbMsgCallbackWrapper {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
callback.onFailure(new RuleEngineException(t.getMessage())); callback.onFailure(new RuleEngineException(t.getMessage(), t));
} }
} }

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java

@ -81,9 +81,10 @@ class TbMsgTypeSwitchNodeTest {
var msg = resultMsgs.get(i); var msg = resultMsgs.get(i);
assertThat(msg).isNotNull(); assertThat(msg).isNotNull();
assertThat(msg.getType()).isNotNull(); assertThat(msg.getType()).isNotNull();
assertThat(msg.getType()).isEqualTo(msg.getInternalType().name());
assertThat(msg).isSameAs(tbMsgList.get(i)); assertThat(msg).isSameAs(tbMsgList.get(i));
assertThat(resultNodeConnections.get(i)) assertThat(resultNodeConnections.get(i))
.isEqualTo(TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); .isEqualTo(msg.getInternalType().getRuleNodeConnection());
} }
} }

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

@ -41,7 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache: cache:
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"

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

@ -68,7 +68,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache: cache:
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"

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

@ -41,7 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache: cache:
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"

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

@ -41,7 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache: cache:
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"

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

@ -41,7 +41,7 @@ zk:
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache: cache:
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"

12
ui-ngx/src/app/core/http/alarm.service.ts

@ -52,12 +52,12 @@ export class AlarmService {
return this.http.post<Alarm>('/api/alarm', alarm, defaultHttpOptionsFromConfig(config)); return this.http.post<Alarm>('/api/alarm', alarm, defaultHttpOptionsFromConfig(config));
} }
public ackAlarm(alarmId: string, config?: RequestConfig): Observable<void> { public ackAlarm(alarmId: string, config?: RequestConfig): Observable<AlarmInfo> {
return this.http.post<void>(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config)); return this.http.post<AlarmInfo>(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config));
} }
public clearAlarm(alarmId: string, config?: RequestConfig): Observable<void> { public clearAlarm(alarmId: string, config?: RequestConfig): Observable<AlarmInfo> {
return this.http.post<void>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); return this.http.post<AlarmInfo>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config));
} }
public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable<void> { public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable<void> {
@ -68,8 +68,8 @@ export class AlarmService {
return this.http.delete<void>(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config)); return this.http.delete<void>(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config));
} }
public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<void> { public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<boolean> {
return this.http.delete<void>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); return this.http.delete<boolean>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config));
} }
public getAlarms(query: AlarmQuery, public getAlarms(query: AlarmQuery,

18
ui-ngx/src/app/core/http/attribute.service.ts

@ -50,10 +50,19 @@ export class AttributeService {
} }
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> { startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true,
config?: RequestConfig): Observable<any> {
const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`;
`?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`; if (isDefinedAndNotNull(deleteAllDataForKeys)) {
url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`;
}
if (isDefinedAndNotNull(rewriteLatestIfDeleted)) {
url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`;
}
if (isDefinedAndNotNull(deleteLatest)) {
url += `&deleteLatest=${deleteLatest}`;
}
if (isDefinedAndNotNull(startTs)) { if (isDefinedAndNotNull(startTs)) {
url += `&startTs=${startTs}`; url += `&startTs=${startTs}`;
} }
@ -103,7 +112,8 @@ export class AttributeService {
}); });
let deleteEntityTimeseriesObservable: Observable<any>; let deleteEntityTimeseriesObservable: Observable<any>;
if (deleteTimeseries.length) { if (deleteTimeseries.length) {
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config); deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true,
null, null, false, true, config);
} else { } else {
deleteEntityTimeseriesObservable = of(null); deleteEntityTimeseriesObservable = of(null);
} }

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

@ -26,17 +26,17 @@ import { DatePipe } from '@angular/common';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { Observable } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { import {
AlarmInfo, AlarmInfo,
AlarmQuery,
AlarmQueryV2, AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
AlarmsMode, AlarmsMode,
AlarmStatus,
alarmStatusTranslations alarmStatusTranslations
} from '@app/shared/models/alarm.models'; } from '@app/shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
@ -46,7 +46,7 @@ import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AlarmDetailsDialogData AlarmDetailsDialogData
} from '@home/components/alarm/alarm-details-dialog.component'; } from '@home/components/alarm/alarm-details-dialog.component';
import { DAY, forAllTimeInterval, historyInterval } from '@shared/models/time/time.models'; import { forAllTimeInterval } from '@shared/models/time/time.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@ -93,7 +93,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.pageMode = pageMode; this.pageMode = pageMode;
this.defaultTimewindowInterval = forAllTimeInterval(); this.defaultTimewindowInterval = forAllTimeInterval();
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = true;
this.searchEnabled = true; this.searchEnabled = true;
this.addEnabled = false; this.addEnabled = false;
this.entitiesDeleteEnabled = false; this.entitiesDeleteEnabled = false;
@ -155,6 +155,27 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
onAction: ($event, entity) => this.showAlarmDetails(entity) onAction: ($event, entity) => this.showAlarmDetails(entity)
} }
); );
this.groupActionDescriptors.push(
{
name: this.translate.instant('alarm.acknowledge'),
icon: 'done',
isEnabled: true,
onAction: ($event, entities) => this.ackAlarms($event, entities)
},
{
name: this.translate.instant('alarm.clear'),
icon: 'clear',
isEnabled: true,
onAction: ($event, entities) => this.clearAlarms($event, entities)
},
{
name: this.translate.instant('alarm.delete'),
icon: 'delete',
isEnabled: true,
onAction: ($event, entities) => this.deleteAlarms($event, entities)
}
)
} }
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> { fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
@ -294,4 +315,103 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
}); });
} }
ackAlarms($event: Event, alarms: Array<AlarmInfo>) {
if ($event) {
$event.stopPropagation();
}
const unacknowledgedAlarms = alarms.filter(alarm => {
return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = '';
let content = '';
if (!unacknowledgedAlarms.length) {
title = this.translate.instant('alarm.selected-alarms', {count: alarms.length});
content = this.translate.instant('alarm.selected-alarms-are-acknowledged');
this.dialogService.alert(
title,
content).subscribe();
} else {
title = this.translate.instant('alarm.aknowledge-alarms-title', {count: unacknowledgedAlarms.length});
content = this.translate.instant('alarm.aknowledge-alarms-text', {count: unacknowledgedAlarms.length});
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
const tasks: Observable<AlarmInfo>[] = [];
for (const alarm of unacknowledgedAlarms) {
tasks.push(this.alarmService.ackAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.updateData();
});
}
});
}
}
clearAlarms($event: Event, alarms: Array<AlarmInfo>) {
if ($event) {
$event.stopPropagation();
}
const activeAlarms = alarms.filter(alarm => {
return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = '';
let content = '';
if (!activeAlarms.length) {
title = this.translate.instant('alarm.selected-alarms', {count: alarms.length});
content = this.translate.instant('alarm.selected-alarms-are-cleared');
this.dialogService.alert(
title,
content
).subscribe();
} else {
title = this.translate.instant('alarm.clear-alarms-title', {count: activeAlarms.length});
content = this.translate.instant('alarm.clear-alarms-text', {count: activeAlarms.length});
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
const tasks: Observable<AlarmInfo>[] = [];
for (const alarm of activeAlarms) {
tasks.push(this.alarmService.clearAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.updateData();
});
}
});
}
}
deleteAlarms($event: Event, alarms: Array<AlarmInfo>) {
if ($event) {
$event.stopPropagation();
}
const title = this.translate.instant('alarm.delete-alarms-title', {count: alarms.length});
const content = this.translate.instant('alarm.delete-alarms-text', {count: alarms.length});
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
const tasks: Observable<boolean>[] = [];
for (const alarm of alarms) {
tasks.push(this.alarmService.deleteAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.updateData();
});
}
});
}
} }

8
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -85,12 +85,12 @@
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }} 'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
</span> </span>
<span fxFlex></span> <span fxFlex></span>
<button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)" <button [fxShow]="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
class="button-widget-action" class="button-widget-action"
mat-icon-button [disabled]="isLoading$ | async" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}" matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteAttributes($event)"> (click)="deleteTelemetry($event)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button mat-raised-button color="accent" <button mat-raised-button color="accent"
@ -198,6 +198,10 @@
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"> <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</span> </span>
<button mat-icon-button *ngIf="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY"
(click)="deleteTimeseries($event, attribute)">
<mat-icon>delete</mat-icon>
</button>
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>

92
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -49,6 +49,7 @@ import {
LatestTelemetry, LatestTelemetry,
TelemetryType, TelemetryType,
telemetryTypeTranslations, telemetryTypeTranslations,
TimeseriesDeleteStrategy,
toTelemetryType toTelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
@ -82,10 +83,15 @@ import {
AddWidgetToDashboardDialogComponent, AddWidgetToDashboardDialogComponent,
AddWidgetToDashboardDialogData AddWidgetToDashboardDialogData
} from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; } from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone, isUndefinedOrNull } from '@core/utils';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import {
DELETE_TIMESERIES_PANEL_DATA,
DeleteTimeseriesPanelComponent,
DeleteTimeseriesPanelData
} from '@home/components/attribute/delete-timeseries-panel.component';
@Component({ @Component({
@ -379,6 +385,82 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
}); });
} }
deleteTimeseries($event: Event, telemetry?: AttributeData) {
if ($event) {
$event.stopPropagation();
}
const isMultipleDeletion = isUndefinedOrNull(telemetry) && this.dataSource.selection.selected.length > 1;
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxWidth: 488,
width: '100%'
});
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: DELETE_TIMESERIES_PANEL_DATA,
useValue: {
isMultipleDeletion
} as DeleteTimeseriesPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(DeleteTimeseriesPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result !== null) {
const result = componentRef.instance.result;
const deleteTimeseries = telemetry ? [telemetry]: this.dataSource.selection.selected;
let deleteAllDataForKeys = false;
let rewriteLatestIfDeleted = false;
let startTs = null;
let endTs = null;
let deleteLatest = true;
switch (result.strategy) {
case TimeseriesDeleteStrategy.DELETE_ALL_DATA:
deleteAllDataForKeys = true;
break;
case TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE:
deleteAllDataForKeys = true;
deleteLatest = false;
break;
case TimeseriesDeleteStrategy.DELETE_LATEST_VALUE:
rewriteLatestIfDeleted = result.rewriteLatest;
startTs = deleteTimeseries[0].lastUpdateTs;
endTs = startTs + 1;
break;
case TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD:
startTs = result.startDateTime.getTime();
endTs = result.endDateTime.getTime();
rewriteLatestIfDeleted = result.rewriteLatest;
break;
}
this.attributeService.deleteEntityTimeseries(this.entityIdValue, deleteTimeseries, deleteAllDataForKeys,
startTs, endTs, rewriteLatestIfDeleted, deleteLatest)
.subscribe(() => this.reloadAttributes());
}
});
}
deleteAttributes($event: Event) { deleteAttributes($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -403,6 +485,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
} }
} }
deleteTelemetry($event: Event) {
if (this.attributeScope === this.latestTelemetryTypes.LATEST_TELEMETRY) {
this.deleteTimeseries($event);
} else {
this.deleteAttributes($event);
}
}
enterWidgetMode() { enterWidgetMode() {
this.mode = 'widget'; this.mode = 'widget';
this.widgetsList = []; this.widgetsList = [];

68
ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html

@ -0,0 +1,68 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-toolbar>
<h2>{{ "attribute.delete-timeseries.delete-strategy" | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
type="button"
(click)="cancel()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<form [formGroup]="deleteTimeseriesFormGroup" class="mat-content mat-padding tb-form-settings">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.strategy</mat-label>
<mat-select formControlName="strategy">
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy">
{{ strategiesTranslationsMap.get(strategy) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="isPeriodStrategy()" class="tb-select-interval">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
</mat-form-field>
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
</mat-form-field>
</div>
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest" *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()">
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
</mat-slide-toggle>
</form>
<div fxLayout="row" class="tb-panel-actions">
<span fxFlex></span>
<button mat-button color="primary"
type="button"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="delete()"
[disabled]="deleteTimeseriesFormGroup.invalid">
{{ 'action.apply' | translate }}
</button>
</div>

44
ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.scss

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2023 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 '../../../../../scss/constants';
:host {
width: 100%;
.mat-toolbar {
background: none;
}
.tb-form-settings {
flex-direction: column;
gap: 16px;
padding-top: 0;
}
.tb-select-interval {
display: flex;
flex-direction: row;
gap: 16px;
@media #{$mat-xs} {
flex-direction: column;
gap: 0;
}
}
.tb-slide-toggle {
margin-bottom: 8px;
}
}

151
ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.ts

@ -0,0 +1,151 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken, OnDestroy, OnInit } from '@angular/core';
import { OverlayRef } from '@angular/cdk/overlay';
import {
TimeseriesDeleteStrategy,
timeseriesDeleteStrategyTranslations
} from '@shared/models/telemetry/telemetry.models';
import { MINUTE } from '@shared/models/time/time.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export const DELETE_TIMESERIES_PANEL_DATA = new InjectionToken<any>('DeleteTimeseriesPanelData');
export interface DeleteTimeseriesPanelData {
isMultipleDeletion: boolean;
}
export interface DeleteTimeseriesPanelResult {
strategy: TimeseriesDeleteStrategy;
startDateTime: Date;
endDateTime: Date;
rewriteLatest: boolean;
}
@Component({
selector: 'tb-delete-timeseries-panel',
templateUrl: './delete-timeseries-panel.component.html',
styleUrls: ['./delete-timeseries-panel.component.scss']
})
export class DeleteTimeseriesPanelComponent implements OnInit, OnDestroy {
deleteTimeseriesFormGroup: FormGroup;
result: DeleteTimeseriesPanelResult = null;
strategiesTranslationsMap = timeseriesDeleteStrategyTranslations;
private multipleDeletionStrategies = new Set<TimeseriesDeleteStrategy>([
TimeseriesDeleteStrategy.DELETE_ALL_DATA,
TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE
]);
private destroy$ = new Subject<void>();
constructor(@Inject(DELETE_TIMESERIES_PANEL_DATA) private data: DeleteTimeseriesPanelData,
private overlayRef: OverlayRef,
private fb: FormBuilder) { }
ngOnInit(): void {
const today = new Date();
if (this.data.isMultipleDeletion) {
this.strategiesTranslationsMap = new Map(Array.from(this.strategiesTranslationsMap)
.filter(([strategy]) => this.multipleDeletionStrategies.has(strategy)))
}
this.deleteTimeseriesFormGroup = this.fb.group({
strategy: [TimeseriesDeleteStrategy.DELETE_ALL_DATA],
startDateTime: [
{ value: new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()), disabled: true },
Validators.required
],
endDateTime: [{ value: today, disabled: true }, Validators.required],
rewriteLatest: [true]
})
this.deleteTimeseriesFormGroup.get('strategy').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) {
this.deleteTimeseriesFormGroup.get('startDateTime').enable({onlySelf: true, emitEvent: false});
this.deleteTimeseriesFormGroup.get('endDateTime').enable({onlySelf: true, emitEvent: false});
} else {
this.deleteTimeseriesFormGroup.get('startDateTime').disable({onlySelf: true, emitEvent: false});
this.deleteTimeseriesFormGroup.get('endDateTime').disable({onlySelf: true, emitEvent: false});
}
})
this.deleteTimeseriesFormGroup.get('startDateTime').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => this.onStartDateTimeChange(value));
this.deleteTimeseriesFormGroup.get('endDateTime').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => this.onEndDateTimeChange(value));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
delete(): void {
if (this.deleteTimeseriesFormGroup.valid) {
this.result = this.deleteTimeseriesFormGroup.value;
this.overlayRef.dispose();
} else {
this.deleteTimeseriesFormGroup.markAllAsTouched();
}
}
cancel(): void {
this.overlayRef.dispose();
}
isPeriodStrategy(): boolean {
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD;
}
isDeleteLatestStrategy(): boolean {
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE;
}
private onStartDateTimeChange(newStartDateTime: Date) {
if (newStartDateTime) {
const endDateTimeTs = this.deleteTimeseriesFormGroup.get('endDateTime').value.getTime();
if (newStartDateTime.getTime() >= endDateTimeTs) {
this.deleteTimeseriesFormGroup.get('startDateTime')
.patchValue(new Date(endDateTimeTs - MINUTE), {onlySelf: true, emitEvent: false});
} else {
this.deleteTimeseriesFormGroup.get('startDateTime')
.patchValue(newStartDateTime, {onlySelf: true, emitEvent: false});
}
}
}
private onEndDateTimeChange(newEndDateTime: Date) {
if (newEndDateTime) {
const startDateTimeTs = this.deleteTimeseriesFormGroup.get('startDateTime').value.getTime();
if (newEndDateTime.getTime() <= startDateTimeTs) {
this.deleteTimeseriesFormGroup.get('endDateTime')
.patchValue(new Date(startDateTimeTs + MINUTE), {onlySelf: true, emitEvent: false});
} else {
this.deleteTimeseriesFormGroup.get('endDateTime')
.patchValue(newEndDateTime, {onlySelf: true, emitEvent: false});
}
}
}
}

4
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -40,6 +40,7 @@ import {
EventContentDialogData EventContentDialogData
} from '@home/components/event/event-content-dialog.component'; } from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils'; import { isEqual, sortObjectKeys } from '@core/utils';
import { historyInterval, MINUTE } from '@shared/models/time/time.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
@ -92,6 +93,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
this.useTimePageLink = true; this.useTimePageLink = true;
this.defaultTimewindowInterval = historyInterval(MINUTE * 15);
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = false;
this.searchEnabled = false; this.searchEnabled = false;
@ -179,7 +181,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
updateColumns(updateTableColumns: boolean = false): void { updateColumns(updateTableColumns: boolean = false): void {
this.columns = []; this.columns = [];
this.columns.push( this.columns.push(
new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '120px'), new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '120px', 'yyyy-MM-dd HH:mm:ss.SSS'),
new EntityTableColumn<Event>('server', 'event.server', '100px', new EntityTableColumn<Event>('server', 'event.server', '100px',
(entity) => entity.body.server, entity => ({}), false)); (entity) => entity.body.server, entity => ({}), false));
switch (this.eventType) { switch (this.eventType) {

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

@ -177,6 +177,7 @@ import {
} from '@home/components/widget/action/manage-widget-actions-dialog.component'; } from '@home/components/widget/action/manage-widget-actions-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -205,6 +206,7 @@ import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/ba
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
DeleteTimeseriesPanelComponent,
AliasesEntitySelectPanelComponent, AliasesEntitySelectPanelComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,

70
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html

@ -15,49 +15,47 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup"> <div class="tb-form-panel no-border no-padding">
<div class="container"> <div class="tb-form-table" [formGroup]="relationFiltersFormGroup">
<div class="body" [fxShow]="relationFiltersFormArray.length"> <div class="tb-form-table-header">
<div style="margin-bottom: 10px;" class="row" fxFlex fxLayout="row" <div class="tb-form-table-header-cell flex-50">{{ 'relation.type' | translate }}</div>
fxLayoutAlign="start center" formArrayName="relationFilters" <div class="tb-form-table-header-cell flex-50">{{ 'entity.entity-types' | translate }}</div>
<div class="tb-form-table-header-cell actions-header"></div>
</div>
<div class="tb-form-table-body" formArrayName="relationFilters">
<div class="tb-form-table-row"
*ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index"> *ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <tb-relation-type-autocomplete subscriptSizing="dynamic"
<div class="input-block" fxFlex fxLayout="row" fxLayoutGap="28px" fxLayoutAlign="start center" style="margin-right: 8px"> class="flex-50"
<tb-relation-type-autocomplete fxFlex="50" [additionalClasses]="['tb-inline-field']"
subscriptSizing="dynamic" appearance="outline"
style="min-width: 200px;" [formControl]="relationFilterControl.get('relationType')">
[formControl]="relationFilterControl.get('relationType')"> </tb-relation-type-autocomplete>
</tb-relation-type-autocomplete> <tb-entity-type-list class="entity-type-list flex-50" subscriptSizing="dynamic" appearance="outline"
<tb-entity-type-list fxFlex="50" [additionalClasses]="['tb-inline-field', 'tb-chips', 'flex']"
subscriptSizing="dynamic" filledInputPlaceholder="{{ 'entity.add-entity-type' | translate }}"
[label]="'entity.entity-types' | translate" [allowedEntityTypes]="allowedEntityTypes"
floatLabel="always" [formControl]="relationFilterControl.get('entityTypes')">
[allowedEntityTypes]="allowedEntityTypes" </tb-entity-type-list>
[formControl]="relationFilterControl.get('entityTypes')"> <div class="tb-form-table-row-cell-buttons">
</tb-entity-type-list> <button type="button"
</div> mat-icon-button
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter($index)" (click)="removeFilter($index)"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltip="{{ 'relation.remove-relation-filter' | translate }}" matTooltip="{{ 'relation.remove-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div>
</div>
<div [fxShow]="!relationFiltersFormArray.length">
<span fxLayoutAlign="center center" class="tb-prompt" translate>relation.any-relation</span>
</div> </div>
</div> </div>
</div> </div>
<div class="any-filter" [fxShow]="!relationFiltersFormArray.length"> <div>
<span fxLayoutAlign="center center" <button type="button" mat-stroked-button color="primary" (click)="addFilter()">
class="tb-prompt" translate>relation.any-relation</span> {{ 'filter.add' | translate }}
</button>
</div> </div>
<button mat-stroked-button color="primary"
class="add-button"
[disabled]="isLoading$ | async"
(click)="addFilter()"
type="button"
matTooltip="{{ 'relation.add-relation-filter' | translate }}"
matTooltipPosition="above">
{{ 'relation.add-relation-filter' | translate }}
</button>
</div> </div>

44
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.scss

@ -14,43 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-relation-filters { .flex-50 {
max-width: calc(100vw - 48px); flex: 1 1 50%;
margin-top: 2px; }
overflow: hidden;
.container{
width: 100%;
}
.map-label {
font-weight: 400;
font-size: 12px;
}
.body {
max-height: 363px;
overflow: auto;
.row {
padding-top: 5px;
.input-block {
border: 1px solid #E0E0E0;
width: 100%;
border-radius: 6px;
padding: 24px;
align-items: center;
}
}
}
.any-filter{ .actions-header {
margin: 10px 0 20px; width: 40px
} }
.add-button { .entity-type-list {
margin: 5px 0px 15px; display: flex;
}
} }
} }

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row"> <div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row no-padding-right">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic"> <mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest"> <mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option> <mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>

4
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -18,7 +18,7 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title">{{ panelTitle }}</div> <div class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header"> <div class="tb-form-table-header no-padding-right">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div> <div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div> <div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
@ -32,7 +32,7 @@
[cdkDropListDisabled]="!dragEnabled" [cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)"> (cdkDropListDropped)="keyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-form-table-row tb-draggable" class="tb-form-table-row tb-draggable no-padding-right"
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;"> *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex <tb-data-key-row fxFlex
[formControl]="keyControl" [formControl]="keyControl"

4
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -903,7 +903,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (res) { if (res) {
const tasks: Observable<void>[] = []; const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) { for (const alarmId of alarmIds) {
tasks.push(this.alarmService.ackAlarm(alarmId)); tasks.push(this.alarmService.ackAlarm(alarmId));
} }
@ -959,7 +959,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (res) { if (res) {
const tasks: Observable<void>[] = []; const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) { for (const alarmId of alarmIds) {
tasks.push(this.alarmService.clearAlarm(alarmId)); tasks.push(this.alarmService.clearAlarm(alarmId));
} }

4
ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.scss

@ -20,6 +20,10 @@
min-width: 750px !important; min-width: 750px !important;
max-width: 750px !important; max-width: 750px !important;
.mat-mdc-dialog-content {
padding: 8px;
}
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
min-width: 100% !important; min-width: 100% !important;
} }

5
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.scss

@ -27,10 +27,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-bottom: 22px;
@media #{$mat-gt-sm} {
margin-bottom: 24px;
}
} }
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {

13
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="entitySubtypeListFormGroup" [ngClass]="label ? '' : 'tb-chip-list'" class="mat-block"> <mat-form-field [formGroup]="entitySubtypeListFormGroup" [floatLabel]="floatLabel"
[class]="label ? '' : 'tb-chip-list'" class="mat-block">
<mat-label *ngIf="label">{{ label }}</mat-label> <mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #chipList formControlName="entitySubtypeList"> <mat-chip-grid #chipList formControlName="entitySubtypeList">
<mat-chip-row <mat-chip-row
@ -38,13 +39,6 @@
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="chipAdd($event)"> (matChipInputTokenEnd)="chipAdd($event)">
</mat-chip-grid> </mat-chip-grid>
<button *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').value?.length > 1"
type="button"
matTooltip="{{'entity.clear-selected-profiles' | translate}}"
matSuffix mat-icon-button
(click)="clearChipGrid()">
<mat-icon color="primary" class="material-icons">close</mat-icon>
</button>
<mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete" <mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete"
class="tb-autocomplete" class="tb-autocomplete"
(optionSelected)="selected($event)" (optionSelected)="selected($event)"
@ -58,6 +52,9 @@
</span> </span>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<div matSuffix>
<ng-content select="[additionalButtons]"></ng-content>
</div>
<mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')"> <mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')">
{{ subtypeListEmptyText | translate }} {{ subtypeListEmptyText | translate }}
</mat-error> </mat-error>

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

@ -15,7 +15,7 @@
/// ///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable, Subscription, throwError } from 'rxjs'; import { Observable, Subscription, throwError } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators'; import { map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -23,14 +23,15 @@ import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntitySubtype, EntityType } from '@shared/models/entity-type.models'; import { EntitySubtype, EntityType } from '@shared/models/entity-type.models';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipGrid } from '@angular/material/chips'; import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { AssetService } from '@core/http/asset.service'; import { AssetService } from '@core/http/asset.service';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
import { EdgeService } from '@core/http/edge.service'; import { EdgeService } from '@core/http/edge.service';
import { EntityViewService } from '@core/http/entity-view.service'; import { EntityViewService } from '@core/http/entity-view.service';
import { BroadcastService } from '@core/services/broadcast.service'; import { BroadcastService } from '@core/services/broadcast.service';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { coerceBoolean } from '@shared/decorators/coercion';
import { FloatLabelType } from '@angular/material/form-field';
@Component({ @Component({
selector: 'tb-entity-subtype-list', selector: 'tb-entity-subtype-list',
@ -46,32 +47,43 @@ import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
}) })
export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
entitySubtypeListFormGroup: UntypedFormGroup; entitySubtypeListFormGroup: FormGroup;
modelValue: Array<string> | null; modelValue: Array<string> | null;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;
} }
@Input() label: string;
@Input() @Input()
@coerceBoolean()
set required(value: boolean) { set required(value: boolean) {
const newVal = coerceBooleanProperty(value); if (this.requiredValue !== value) {
if (this.requiredValue !== newVal) { this.requiredValue = value;
this.requiredValue = newVal;
this.updateValidators(); this.updateValidators();
} }
} }
@Input()
floatLabel: FloatLabelType = 'auto';
@Input()
label: string;
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input() @Input()
entityType: EntityType; entityType: EntityType;
@Input()
emptyInputPlaceholder: string;
@Input()
filledInputPlaceholder: string;
@ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef<HTMLInputElement>; @ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef<HTMLInputElement>;
@ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete; @ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: true}) chipList: MatChipGrid; @ViewChild('chipList', {static: true}) chipList: MatChipGrid;
@ -102,13 +114,14 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private deviceService: DeviceService, private deviceService: DeviceService,
private edgeService: EdgeService, private edgeService: EdgeService,
private entityViewService: EntityViewService, private entityViewService: EntityViewService,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({ this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
entitySubtype: [null] entitySubtype: [null]
}); });
} }
updateValidators() { updateValidators() {
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []); this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []);
this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity(); this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity();
@ -122,7 +135,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
} }
ngOnInit() { ngOnInit() {
switch (this.entityType) { switch (this.entityType) {
case EntityType.ASSET: case EntityType.ASSET:
this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type') this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type')
@ -166,6 +178,13 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
break; break;
} }
if (this.emptyInputPlaceholder) {
this.placeholder = this.emptyInputPlaceholder;
}
if (this.filledInputPlaceholder) {
this.secondaryPlaceholder = this.filledInputPlaceholder;
}
this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges
.pipe( .pipe(
map(value => value ? value : ''), map(value => value ? value : ''),
@ -225,13 +244,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
} }
this.clear(''); this.clear('');
} }
clearChipGrid() {
this.entitySubtypeList = [];
this.modelValue = null;
this.entitySubtypeListFormGroup.get('entitySubtypeList').patchValue([], {emitEvent: true});
}
remove(entitySubtype: string) { remove(entitySubtype: string) {
const index = this.entitySubtypeList.indexOf(entitySubtype); const index = this.entitySubtypeList.indexOf(entitySubtype);
if (index >= 0) { if (index >= 0) {

9
ui-ngx/src/app/shared/components/entity/entity-type-list.component.html

@ -15,7 +15,11 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="entityTypeListFormGroup" class="mat-block" [floatLabel]="floatLabel" subscriptSizing="{{ subscriptSizing }}"> <mat-form-field [formGroup]="entityTypeListFormGroup"
[appearance]="appearance"
class="mat-block" [class]="additionalClasses"
[floatLabel]="floatLabel"
subscriptSizing="{{ subscriptSizing }}">
<mat-label *ngIf="label"> {{ label }}</mat-label> <mat-label *ngIf="label"> {{ label }}</mat-label>
<mat-chip-grid #chipList formControlName="entityTypeList"> <mat-chip-grid #chipList formControlName="entityTypeList">
<mat-chip-row <mat-chip-row
@ -48,6 +52,9 @@
</span> </span>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</div>
<mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')"> <mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')">
{{ 'entity.entity-type-list-empty' | translate }} {{ 'entity.entity-type-list-empty' | translate }}
</mat-error> </mat-error>

57
ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts

@ -15,7 +15,7 @@
/// ///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -25,9 +25,8 @@ import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/mod
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatChipGrid } from '@angular/material/chips'; import { MatChipGrid } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { FloatLabelType, SubscriptSizing } from '@angular/material/form-field'; import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { coerceBoolean } from '@shared/decorators/coercion';
interface EntityTypeInfo { interface EntityTypeInfo {
name: string; name: string;
@ -48,7 +47,7 @@ interface EntityTypeInfo {
}) })
export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit { export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit {
entityTypeListFormGroup: UntypedFormGroup; entityTypeListFormGroup: FormGroup;
modelValue: Array<EntityType> | null; modelValue: Array<EntityType> | null;
@ -57,19 +56,28 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
return this.requiredValue; return this.requiredValue;
} }
@Input() label: string;
@Input() floatLabel: FloatLabelType = 'auto';
@Input() @Input()
@coerceBoolean()
set required(value: boolean) { set required(value: boolean) {
const newVal = coerceBooleanProperty(value); if (this.requiredValue !== value) {
if (this.requiredValue !== newVal) { this.requiredValue = value;
this.requiredValue = newVal;
this.updateValidators(); this.updateValidators();
} }
} }
@Input()
@coerceArray()
additionalClasses: Array<string>;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
label: string;
@Input()
floatLabel: FloatLabelType = 'auto';
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -79,6 +87,12 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
@Input() @Input()
allowedEntityTypes: Array<EntityType | AliasEntityType>; allowedEntityTypes: Array<EntityType | AliasEntityType>;
@Input()
emptyInputPlaceholder: string;
@Input()
filledInputPlaceholder: string;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
ignoreAuthorityFilter: boolean; ignoreAuthorityFilter: boolean;
@ -103,7 +117,7 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
public translate: TranslateService, public translate: TranslateService,
private entityService: EntityService, private entityService: EntityService,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
this.entityTypeListFormGroup = this.fb.group({ this.entityTypeListFormGroup = this.fb.group({
entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []], entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []],
entityType: [null] entityType: [null]
@ -123,11 +137,17 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
} }
ngOnInit() { ngOnInit() {
if (this.emptyInputPlaceholder) {
this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') this.placeholder = this.emptyInputPlaceholder;
: this.translate.instant('entity.any-entity'); } else {
this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') :
this.translate.instant('entity.any-entity');
}
if (this.filledInputPlaceholder) {
this.secondaryPlaceholder = this.filledInputPlaceholder;
} else {
this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type');
}
let entityTypes: Array<EntityType | AliasEntityType>; let entityTypes: Array<EntityType | AliasEntityType>;
if (this.ignoreAuthorityFilter && this.allowedEntityTypes if (this.ignoreAuthorityFilter && this.allowedEntityTypes
&& this.allowedEntityTypes.length) { && this.allowedEntityTypes.length) {
@ -250,5 +270,4 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
this.entityTypeInput.nativeElement.focus(); this.entityTypeInput.nativeElement.focus();
}, 0); }, 0);
} }
} }

10
ui-ngx/src/app/shared/components/help-popup.component.html

@ -30,19 +30,21 @@
</button> </button>
</div> </div>
</fieldset> </fieldset>
<fieldset class="tb-help-popup-button-container" *ngIf="textMode"> <fieldset class="tb-help-popup-button-container" [class.hint-button]="hintMode" *ngIf="textMode">
<div #toggleHelpTextButton <div #toggleHelpTextButton
(click)="toggleHelp()"> (click)="toggleHelp()">
<button mat-button <button mat-button
color="primary" color="primary"
class="tb-help-popup-text-button" class="tb-help-popup-text-button"
[ngClass]="{'mat-mdc-outlined-button mdc-button--outlined': popoverVisible && popoverReady}"> [class]="{'mat-mdc-outlined-button mdc-button--outlined': popoverVisible && popoverReady,
'hint-button': hintMode}">
<span> <span>
<ng-container *ngIf="triggerSafeHtml"> <ng-container *ngIf="triggerSafeHtml">
<span [style]="triggerStyle" [innerHTML]="triggerSafeHtml"></span> <span [style]="triggerStyle" [innerHTML]="triggerSafeHtml"></span>
</ng-container> </ng-container>
<mat-icon *ngIf="!popoverVisible || popoverReady" class="tb-mat-16">open_in_new</mat-icon> <mat-icon *ngIf="(!popoverVisible || popoverReady)"
<mat-spinner *ngIf="popoverVisible && !popoverReady" mode="indeterminate" diameter="16" strokeWidth="2"></mat-spinner> [ngClass]="hintMode ? 'tb-mat-12' : 'tb-mat-16'">open_in_new</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" mode="indeterminate" [diameter]="hintMode ? 12 : 16" strokeWidth="2"></mat-spinner>
</span> </span>
</button> </button>
</div> </div>

18
ui-ngx/src/app/shared/components/help-popup.component.scss

@ -17,6 +17,9 @@
width: initial; width: initial;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
&.hint-button {
line-height: 1;
}
} }
.tb-help-popup-button { .tb-help-popup-button {
@ -65,4 +68,19 @@
vertical-align: middle; vertical-align: middle;
} }
} }
&.hint-button {
padding: 2px 3px;
line-height: 1;
&.mat-mdc-outlined-button {
padding: 1px 2px;
}
.mdc-button__label > span {
.mat-icon {
margin-right: 0;
}
.mat-mdc-progress-spinner {
margin-right: 0;
}
}
}
} }

6
ui-ngx/src/app/shared/components/help-popup.component.ts

@ -28,6 +28,7 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
// eslint-disable-next-line @angular-eslint/component-selector // eslint-disable-next-line @angular-eslint/component-selector
@ -62,6 +63,11 @@ export class HelpPopupComponent implements OnChanges, OnDestroy {
popoverVisible = false; popoverVisible = false;
popoverReady = true; popoverReady = true;
@Input()
@coerceBoolean()
hintMode = false;
triggerSafeHtml: SafeHtml = null; triggerSafeHtml: SafeHtml = null;
textMode = false; textMode = false;

3
ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="relationTypeFormGroup" [floatLabel]="floatLabel" class="mat-block" subscriptSizing="{{ subscriptSizing }}"> <mat-form-field [formGroup]="relationTypeFormGroup" [appearance]="appearance" [floatLabel]="floatLabel"
class="mat-block" [class]="additionalClasses" subscriptSizing="{{ subscriptSizing }}">
<mat-label *ngIf="label">{{ label }}</mat-label> <mat-label *ngIf="label">{{ label }}</mat-label>
<input matInput type="text" <input matInput type="text"
#relationTypeInput #relationTypeInput

24
ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts

@ -24,7 +24,8 @@ import { TranslateService } from '@ngx-translate/core';
import { BroadcastService } from '@app/core/services/broadcast.service'; import { BroadcastService } from '@app/core/services/broadcast.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { RelationTypes } from '@app/shared/models/relation.models'; import { RelationTypes } from '@app/shared/models/relation.models';
import { FloatLabelType, SubscriptSizing } from '@angular/material/form-field'; import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-relation-type-autocomplete', selector: 'tb-relation-type-autocomplete',
@ -42,19 +43,22 @@ export class RelationTypeAutocompleteComponent implements ControlValueAccessor,
modelValue: string | null; modelValue: string | null;
private requiredValue: boolean; @Input()
get required(): boolean { label: string;
return this.requiredValue;
}
@Input() label: string; @Input()
@coerceArray()
additionalClasses: Array<string>;
@Input() floatLabel: FloatLabelType = 'auto'; @Input()
appearance: MatFormFieldAppearance = 'fill';
@Input() @Input()
set required(value: boolean) { floatLabel: FloatLabelType = 'auto';
this.requiredValue = coerceBooleanProperty(value);
} @Input()
@coerceBoolean()
required: boolean;
@Input() @Input()
disabled: boolean; disabled: boolean;

10
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -54,7 +54,15 @@
{{ 'common.not-found' | translate }} {{ 'common.not-found' | translate }}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-hint>{{ hint }}</mat-hint> <mat-hint [hidden]="!hint">
{{ hint }}
</mat-hint>
<mat-hint align="end" [hidden]="!matHintEnd.children.length" #matHintEnd>
<ng-content select="[matHintEnd]"></ng-content>
</mat-hint>
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</div>
<mat-error *ngIf="stringItemsForm.get('items').hasError('required')"> <mat-error *ngIf="stringItemsForm.get('items').hasError('required')">
{{ requiredText }} {{ requiredText }}
</mat-error> </mat-error>

18
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -61,6 +61,13 @@ export enum TelemetryFeature {
TIMESERIES = 'TIMESERIES' TIMESERIES = 'TIMESERIES'
} }
export enum TimeseriesDeleteStrategy {
DELETE_ALL_DATA = 'DELETE_ALL_DATA',
DELETE_ALL_DATA_EXCEPT_LATEST_VALUE = 'DELETE_ALL_DATA_EXCEPT_LATEST_VALUE',
DELETE_LATEST_VALUE = 'DELETE_LATEST_VALUE',
DELETE_ALL_DATA_FOR_TIME_PERIOD = 'DELETE_ALL_DATA_FOR_TIME_PERIOD'
}
export type TelemetryType = LatestTelemetry | AttributeScope; export type TelemetryType = LatestTelemetry | AttributeScope;
export const toTelemetryType = (val: string): TelemetryType => { export const toTelemetryType = (val: string): TelemetryType => {
@ -73,7 +80,7 @@ export const toTelemetryType = (val: string): TelemetryType => {
export const telemetryTypeTranslations = new Map<TelemetryType, string>( export const telemetryTypeTranslations = new Map<TelemetryType, string>(
[ [
[LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-latest-telemetry'], [LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-telemetry'],
[AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'], [AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'],
[AttributeScope.SERVER_SCOPE, 'attribute.scope-server'], [AttributeScope.SERVER_SCOPE, 'attribute.scope-server'],
[AttributeScope.SHARED_SCOPE, 'attribute.scope-shared'] [AttributeScope.SHARED_SCOPE, 'attribute.scope-shared']
@ -89,6 +96,15 @@ export const isClientSideTelemetryType = new Map<TelemetryType, boolean>(
] ]
); );
export const timeseriesDeleteStrategyTranslations = new Map<TimeseriesDeleteStrategy, string>(
[
[TimeseriesDeleteStrategy.DELETE_ALL_DATA, 'attribute.delete-timeseries.all-data'],
[TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE, 'attribute.delete-timeseries.all-data-except-latest-value'],
[TimeseriesDeleteStrategy.DELETE_LATEST_VALUE, 'attribute.delete-timeseries.latest-value'],
[TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD, 'attribute.delete-timeseries.all-data-for-time-period']
]
)
export interface AttributeData { export interface AttributeData {
lastUpdateTs?: number; lastUpdateTs?: number;
key: string; key: string;

1
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -632,6 +632,7 @@
"attributes": "Atributs", "attributes": "Atributs",
"latest-telemetry": "Última telemetria", "latest-telemetry": "Última telemetria",
"attributes-scope": "Abast dels atributs del dispositiu", "attributes-scope": "Abast dels atributs del dispositiu",
"scope-telemetry": "Telemetria",
"scope-latest-telemetry": "Última telemetria", "scope-latest-telemetry": "Última telemetria",
"scope-client": "Atributs del Client", "scope-client": "Atributs del Client",
"scope-server": "Atributs del Servidor", "scope-server": "Atributs del Servidor",

1
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -445,6 +445,7 @@
"attributes": "Atributy", "attributes": "Atributy",
"latest-telemetry": "Poslední telemetrie", "latest-telemetry": "Poslední telemetrie",
"attributes-scope": "Rozsah atributů entity", "attributes-scope": "Rozsah atributů entity",
"scope-telemetry": "Telemetrie",
"scope-latest-telemetry": "Poslední telemetrie", "scope-latest-telemetry": "Poslední telemetrie",
"scope-client": "Atributy klienta", "scope-client": "Atributy klienta",
"scope-server": "Atributy serveru", "scope-server": "Atributy serveru",

1
ui-ngx/src/assets/locale/locale.constant-da_DK.json

@ -453,6 +453,7 @@
"attributes": "Attributter", "attributes": "Attributter",
"latest-telemetry": "Seneste telemetri", "latest-telemetry": "Seneste telemetri",
"attributes-scope": "Omfang af entitetsattributter", "attributes-scope": "Omfang af entitetsattributter",
"scope-telemetry": "Telemetri",
"scope-latest-telemetry": "Seneste telemetri", "scope-latest-telemetry": "Seneste telemetri",
"scope-client": "Klientattributter", "scope-client": "Klientattributter",
"scope-server": "Serverattributter", "scope-server": "Serverattributter",

1
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -324,6 +324,7 @@
"attributes": "Eigenschaften", "attributes": "Eigenschaften",
"latest-telemetry": "Neueste Telemetrie", "latest-telemetry": "Neueste Telemetrie",
"attributes-scope": "Entitätseigenschaftsbereich", "attributes-scope": "Entitätseigenschaftsbereich",
"scope-telemetry": "Telemetrie",
"scope-latest-telemetry": "Neueste Telemetrie", "scope-latest-telemetry": "Neueste Telemetrie",
"scope-client": "Client Eigenschaften", "scope-client": "Client Eigenschaften",
"scope-server": "Server Eigenschaften", "scope-server": "Server Eigenschaften",

1
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -291,6 +291,7 @@
"attributes": "Χαρακτηριστικά", "attributes": "Χαρακτηριστικά",
"latest-telemetry": "Τελευταία τηλεμετρία", "latest-telemetry": "Τελευταία τηλεμετρία",
"attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας", "attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας",
"scope-telemetry": "Τηλεμετρία",
"scope-latest-telemetry": "Τελευταία τηλεμετρία", "scope-latest-telemetry": "Τελευταία τηλεμετρία",
"scope-client": "Χαρακτηριστικά Client", "scope-client": "Χαρακτηριστικά Client",
"scope-server": "Χαρακτηριστικά Server", "scope-server": "Χαρακτηριστικά Server",

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

@ -512,6 +512,7 @@
"severity-indeterminate": "Indeterminate", "severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge", "acknowledge": "Acknowledge",
"clear": "Clear", "clear": "Clear",
"delete": "Delete",
"search": "Search alarms", "search": "Search alarms",
"selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} } selected", "selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} } selected",
"no-data": "No data to display", "no-data": "No data to display",
@ -522,10 +523,14 @@
"aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, =1 {1 alarm} other {# alarms} }?", "aknowledge-alarms-text": "Are you sure you want to acknowledge { count, plural, =1 {1 alarm} other {# alarms} }?",
"aknowledge-alarm-title": "Acknowledge Alarm", "aknowledge-alarm-title": "Acknowledge Alarm",
"aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?", "aknowledge-alarm-text": "Are you sure you want to acknowledge Alarm?",
"selected-alarms-are-acknowledged": "Selected alarms are already acknowledged",
"clear-alarms-title": "Clear { count, plural, =1 {1 alarm} other {# alarms} }", "clear-alarms-title": "Clear { count, plural, =1 {1 alarm} other {# alarms} }",
"clear-alarms-text": "Are you sure you want to clear { count, plural, =1 {1 alarm} other {# alarms} }?", "clear-alarms-text": "Are you sure you want to clear { count, plural, =1 {1 alarm} other {# alarms} }?",
"clear-alarm-title": "Clear Alarm", "clear-alarm-title": "Clear Alarm",
"clear-alarm-text": "Are you sure you want to clear Alarm?", "clear-alarm-text": "Are you sure you want to clear Alarm?",
"delete-alarms-title": "Delete { count, plural, =1 {1 alarm} other {# alarms} }",
"delete-alarms-text": "Are you sure you want to delete { count, plural, =1 {1 alarm} other {# alarms} }?",
"selected-alarms-are-cleared": "Selected alarms are already cleared",
"alarm-status-filter": "Alarm Status Filter", "alarm-status-filter": "Alarm Status Filter",
"alarm-filter-title": "Alarm Filter", "alarm-filter-title": "Alarm Filter",
"assigned": "Assigned", "assigned": "Assigned",
@ -699,6 +704,7 @@
"latest-telemetry": "Latest telemetry", "latest-telemetry": "Latest telemetry",
"no-latest-telemetry": "No latest telemetry", "no-latest-telemetry": "No latest telemetry",
"attributes-scope": "Entity attributes scope", "attributes-scope": "Entity attributes scope",
"scope-telemetry": "Telemetry",
"scope-latest-telemetry": "Latest telemetry", "scope-latest-telemetry": "Latest telemetry",
"scope-client": "Client attributes", "scope-client": "Client attributes",
"scope-server": "Server attributes", "scope-server": "Server attributes",
@ -726,7 +732,19 @@
"no-telemetry-text": "No telemetry found", "no-telemetry-text": "No telemetry found",
"copy-key": "Copy key", "copy-key": "Copy key",
"copy-value": "Copy value", "copy-value": "Copy value",
"add-telemetry": "Add telemetry" "add-telemetry": "Add telemetry",
"copy-value": "Copy value",
"delete-timeseries": {
"start-time": "Start time",
"ends-on": "Ends on",
"strategy": "Strategy",
"delete-strategy": "Delete strategy",
"all-data": "Delete all data",
"all-data-except-latest-value": "Delete all data except latest value",
"latest-value": "Delete latest value",
"all-data-for-time-period": "Delete all data for time period",
"rewrite-latest-value": "Rewrite latest value"
}
}, },
"api-usage": { "api-usage": {
"api-features": "API features", "api-features": "API features",
@ -2035,6 +2053,7 @@
"entity-types": "Entity types", "entity-types": "Entity types",
"entity-type-list": "Entity type list", "entity-type-list": "Entity type list",
"any-entity": "Any entity", "any-entity": "Any entity",
"add-entity-type": "Add entity type",
"enter-entity-type": "Enter entity type", "enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' were found.", "no-entities-matching": "No entities matching '{{entity}}' were found.",
"no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
@ -3329,6 +3348,7 @@
"delete-from-relations-title": "Are you sure you want to delete { count, plural, =1 {1 relation} other {# relations} }?", "delete-from-relations-title": "Are you sure you want to delete { count, plural, =1 {1 relation} other {# relations} }?",
"delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.",
"remove-relation-filter": "Remove relation filter", "remove-relation-filter": "Remove relation filter",
"remove-filter": "Remove filter",
"add-relation-filter": "Add relation filter", "add-relation-filter": "Add relation filter",
"any-relation": "Any relation", "any-relation": "Any relation",
"relation-filters": "Relation filters", "relation-filters": "Relation filters",

1
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -667,6 +667,7 @@
"attributes": "Atributos", "attributes": "Atributos",
"latest-telemetry": "Última telemetría", "latest-telemetry": "Última telemetría",
"attributes-scope": "Alcance de los atributos del dispositivo", "attributes-scope": "Alcance de los atributos del dispositivo",
"scope-telemetry": "Telemetría",
"scope-latest-telemetry": "Última telemetría", "scope-latest-telemetry": "Última telemetría",
"scope-client": "Atributos de Cliente", "scope-client": "Atributos de Cliente",
"scope-server": "Atributos de Servidor", "scope-server": "Atributos de Servidor",

1
ui-ngx/src/assets/locale/locale.constant-fa_IR.json

@ -254,6 +254,7 @@
"attributes": "ويژگي ها", "attributes": "ويژگي ها",
"latest-telemetry": "آخرين سنجش", "latest-telemetry": "آخرين سنجش",
"attributes-scope": "حوزه ويژگي هاي موجودي", "attributes-scope": "حوزه ويژگي هاي موجودي",
"scope-telemetry": "تله متری",
"scope-latest-telemetry": "آخرين سنجش", "scope-latest-telemetry": "آخرين سنجش",
"scope-client": "ويژگي هاي مشتري", "scope-client": "ويژگي هاي مشتري",
"scope-server": "ويژگي هاي سِروِر", "scope-server": "ويژگي هاي سِروِر",

1
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -459,6 +459,7 @@
"next-widget": "Widget suivant", "next-widget": "Widget suivant",
"prev-widget": "Widget précédent", "prev-widget": "Widget précédent",
"scope-client": "Attributs du client", "scope-client": "Attributs du client",
"scope-telemetry": "Télémétrie",
"scope-latest-telemetry": "Dernière télémétrie", "scope-latest-telemetry": "Dernière télémétrie",
"scope-server": "Attributs du serveur", "scope-server": "Attributs du serveur",
"scope-shared": "Attributs partagés", "scope-shared": "Attributs partagés",

1
ui-ngx/src/assets/locale/locale.constant-it_IT.json

@ -276,6 +276,7 @@
"attributes": "Attributi", "attributes": "Attributi",
"latest-telemetry": "Ultima telemetria", "latest-telemetry": "Ultima telemetria",
"attributes-scope": "Visibilità attributi entità", "attributes-scope": "Visibilità attributi entità",
"scope-telemetry": "Telemetria",
"scope-latest-telemetry": "Ultima telemetria", "scope-latest-telemetry": "Ultima telemetria",
"scope-client": "Attributi client", "scope-client": "Attributi client",
"scope-server": "Attributi server", "scope-server": "Attributi server",

1
ui-ngx/src/assets/locale/locale.constant-ja_JP.json

@ -244,6 +244,7 @@
"attributes": "属性", "attributes": "属性",
"latest-telemetry": "最新テレメトリ", "latest-telemetry": "最新テレメトリ",
"attributes-scope": "エンティティ属性のスコープ", "attributes-scope": "エンティティ属性のスコープ",
"scope-telemetry": "テレメトリー",
"scope-latest-telemetry": "最新テレメトリ", "scope-latest-telemetry": "最新テレメトリ",
"scope-client": "クライアントの属性", "scope-client": "クライアントの属性",
"scope-server": "サーバーの属性", "scope-server": "サーバーの属性",

1
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -290,6 +290,7 @@
"attributes": "ატრიბუტები", "attributes": "ატრიბუტები",
"latest-telemetry": "უახლესი ტელემეტრია", "latest-telemetry": "უახლესი ტელემეტრია",
"attributes-scope": "ობიექტის ატრიბუტების ფარგლები", "attributes-scope": "ობიექტის ატრიბუტების ფარგლები",
"scope-telemetry": "ტელემეტრია",
"scope-latest-telemetry": "უახლესი ტელემეტრია", "scope-latest-telemetry": "უახლესი ტელემეტრია",
"scope-client": "კლიენტის ატრიბუტები", "scope-client": "კლიენტის ატრიბუტები",
"scope-server": "სერვერის ატრიბუტები", "scope-server": "სერვერის ატრიბუტები",

1
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -408,6 +408,7 @@
"attributes": "속성", "attributes": "속성",
"latest-telemetry": "최근 데이터", "latest-telemetry": "최근 데이터",
"attributes-scope": "장치 속성 범위", "attributes-scope": "장치 속성 범위",
"scope-telemetry": "원격 측정",
"scope-latest-telemetry": "최근 데이터", "scope-latest-telemetry": "최근 데이터",
"scope-client": "클라이언트 속성", "scope-client": "클라이언트 속성",
"scope-server": "서버 속성", "scope-server": "서버 속성",

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

Loading…
Cancel
Save