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) {
msg.getCallback().onFailure(rne);
} 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);
} catch (Exception 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 = 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,
(result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope));
(result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope));
}
@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_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@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,
(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 {
SecurityUser user = getCurrentUser();
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)",
@ -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_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
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)",
@ -462,7 +462,9 @@ public class TelemetryController extends BaseController {
notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
" Use 'deleteAllDataForKeys' to delete all time-series data." +
" 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,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ -486,14 +488,16 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "startTs", required = false) Long startTs,
@ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.")
@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.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
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,
Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException {
Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException {
List<String> keys = toKeysList(keysStr);
if (keys.isEmpty()) {
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) -> {
List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
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<>() {
@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);");
} 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;");
}

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

@ -363,7 +363,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
callback.onSuccess();
}
} 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 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.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -57,8 +60,23 @@ public class TbMsgPackCallback implements TbMsgCallback {
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
public void onFailure(RuleEngineException e) {
if (ExceptionUtil.lookupExceptionInCause(e, AbstractRateLimitException.class) != null) {
onRateLimit(e);
return;
}
log.trace("[{}] ON FAILURE", id, e);
if (failedMsgTimer != null) {
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);
pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData);
}
@Override
@ -397,12 +396,17 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
void checkAndUpdateState(@Nonnull DeviceId deviceId, @Nonnull DeviceStateData state) {
if (state.getState().isActive()) {
var deviceState = state.getState();
if (deviceState.isActive()) {
updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, state);
} else {
//trying to fix activity state
if (isActive(System.currentTimeMillis(), state.getState())) {
updateActivityState(deviceId, state, state.getState().getLastActivityTime());
if (isActive(System.currentTimeMillis(), deviceState)) {
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}"
# Name of the directory in zookeeper 'filesystem'
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:
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);
}
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 {
MockHttpServletRequestBuilder postRequest = post(urlTemplate);
setJwtToken(postRequest);
@ -828,6 +832,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
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) {
if (params != null && params.length > 0) {
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;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Device;
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.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES;
@DaoSqlTest
@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());
}
@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
public void testValueConstraintValidator() throws Exception {
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.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId;
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.EntityKeyType;
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.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
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.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.UUID;
@ -62,14 +73,32 @@ public class DefaultDeviceStateServiceTest {
PartitionService partitionService;
@Mock
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");
TopicPartitionInfo tpi;
DefaultDeviceStateService service;
TelemetrySubscriptionService telemetrySubscriptionService;
@Before
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
@ -125,4 +154,188 @@ public class DefaultDeviceStateServiceTest {
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;
public class ApiUsageLimitsExceededException extends RuntimeException {
public class ApiUsageLimitsExceededException extends AbstractRateLimitException {
public ApiUsageLimitsExceededException(String 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 {
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);
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) {
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 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;
import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public enum TbMsgType {
@ -39,10 +38,10 @@ public enum TbMsgType {
ENTITY_UNASSIGNED("Entity Unassigned"),
ATTRIBUTES_UPDATED("Attributes Updated"),
ATTRIBUTES_DELETED("Attributes Deleted"),
ALARM(null),
ALARM,
ALARM_ACK("Alarm Acknowledged"),
ALARM_CLEAR("Alarm Cleared"),
ALARM_DELETE(null),
ALARM_DELETE,
ALARM_ASSIGNED("Alarm Assigned"),
ALARM_UNASSIGNED("Alarm Unassigned"),
COMMENT_CREATED("Comment Created"),
@ -50,8 +49,8 @@ public enum TbMsgType {
RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"),
ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"),
ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"),
ENTITY_ASSIGNED_TO_EDGE(null),
ENTITY_UNASSIGNED_FROM_EDGE(null),
ENTITY_ASSIGNED_TO_EDGE,
ENTITY_UNASSIGNED_FROM_EDGE,
TIMESERIES_UPDATED("Timeseries Updated"),
TIMESERIES_DELETED("Timeseries Deleted"),
RPC_QUEUED("RPC Queued"),
@ -65,9 +64,9 @@ public enum TbMsgType {
RELATION_ADD_OR_UPDATE("Relation Added or Updated"),
RELATION_DELETED("Relation Deleted"),
RELATIONS_DELETED("All Relations Deleted"),
PROVISION_SUCCESS(null),
PROVISION_FAILURE(null),
SEND_EMAIL(null),
PROVISION_SUCCESS,
PROVISION_FAILURE,
SEND_EMAIL,
// tellSelfOnly types
GENERATOR_NODE_SELF_MSG(null, true),
@ -76,12 +75,15 @@ public enum TbMsgType {
DEVICE_UPDATE_SELF_MSG(null, true),
DEDUPLICATION_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()
.filter(tbMsgType -> !tbMsgType.isTellSelfOnly())
.map(TbMsgType::getRuleNodeConnection)
.filter(Objects::nonNull)
.filter(connection -> !TbNodeConnectionType.OTHER.equals(connection))
.collect(Collectors.toUnmodifiableList());
@Getter
@ -91,25 +93,16 @@ public enum TbMsgType {
private final boolean tellSelfOnly;
TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) {
this.ruleNodeConnection = ruleNodeConnection;
this.ruleNodeConnection = StringUtils.isNotEmpty(ruleNodeConnection) ? ruleNodeConnection : TbNodeConnectionType.OTHER;
this.tellSelfOnly = tellSelfOnly;
}
TbMsgType(String ruleNodeConnection) {
this.ruleNodeConnection = ruleNodeConnection;
this.tellSelfOnly = false;
this(ruleNodeConnection, false);
}
public static String getRuleNodeConnectionOrElseOther(String msgType) {
if (msgType == null) {
return TbNodeConnectionType.OTHER;
} else {
return Arrays.stream(TbMsgType.values())
.filter(type -> type.name().equals(msgType))
.findFirst()
.map(TbMsgType::getRuleNodeConnection)
.orElse(TbNodeConnectionType.OTHER);
}
TbMsgType() {
this(null, false);
}
}

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.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.NA;
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.ENTITY_ASSIGNED_TO_EDGE;
@ -51,37 +52,36 @@ class TbMsgTypeTest {
DEVICE_UPDATE_SELF_MSG,
DEDUPLICATION_TIMEOUT_SELF_MSG,
DELAY_TIMEOUT_SELF_MSG,
MSG_COUNT_SELF_MSG
MSG_COUNT_SELF_MSG,
NA
);
// backward-compatibility tests
@Test
void getRuleNodeConnectionsTest() {
var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(type.getRuleNodeConnection()).isNull();
assertThat(type.getRuleNodeConnection()).isEqualTo(TbNodeConnectionType.OTHER);
} else {
assertThat(type.getRuleNodeConnection()).isNotNull();
assertThat(type.getRuleNodeConnection()).isNotEqualTo(TbNodeConnectionType.OTHER);
}
}
}
@Test
void getRuleNodeConnectionOrElseOtherTest() {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(null))
.isEqualTo(TbNodeConnectionType.OTHER);
var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name()))
assertThat(type.getRuleNodeConnection())
.isEqualTo(TbNodeConnectionType.OTHER);
} else {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())).isNotNull()
assertThat(type.getRuleNodeConnection()).isNotNull()
.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 long ts;
private final String type;
private final TbMsgType internalType;
private final EntityId originator;
private final CustomerId customerId;
private final TbMsgMetaData metaData;
@ -97,7 +98,7 @@ public final class TbMsg implements Serializable {
*/
@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) {
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);
}
@ -108,7 +109,7 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.5.2", forRemoval = true)
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);
}
@ -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) {
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);
}
@ -126,7 +127,7 @@ public final class TbMsg implements Serializable {
}
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);
}
@ -170,13 +171,13 @@ public final class TbMsg implements Serializable {
*/
@Deprecated(since = "3.5.2")
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);
}
@Deprecated(since = "3.5.2", forRemoval = true)
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);
}
@ -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) {
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);
}
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);
}
@ -222,13 +223,13 @@ public final class TbMsg implements Serializable {
@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) {
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);
}
@Deprecated(since = "3.5.2", forRemoval = true)
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);
}
@ -250,72 +251,77 @@ public final class TbMsg implements Serializable {
*/
@Deprecated(since = "3.5.2")
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);
}
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);
}
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);
}
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);
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
//used for enqueueForTellNext
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);
}
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) {
this.id = id;
this.queueName = queueName;
@ -325,6 +331,7 @@ public final class TbMsg implements Serializable {
this.ts = System.currentTimeMillis();
}
this.type = type;
this.internalType = internalType != null ? internalType : getInternalType(type);
this.originator = originator;
if (customerId == null || customerId.isNullUid()) {
if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) {
@ -410,7 +417,7 @@ public final class TbMsg implements Serializable {
}
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);
} catch (InvalidProtocolBufferException 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) {
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);
}
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);
}
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);
}
@ -468,8 +475,16 @@ public final class TbMsg implements Serializable {
return ts;
}
private TbMsgType getInternalType(String type) {
try {
return TbMsgType.valueOf(type);
} catch (IllegalArgumentException e) {
return TbMsgType.NA;
}
}
public boolean isTypeOf(TbMsgType tbMsgType) {
return tbMsgType != null && tbMsgType.name().equals(this.type);
return internalType.equals(tbMsgType);
}
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();
}
public RuleEngineException(String message, Throwable t) {
super(message != null ? message : "Unknown", t);
ts = System.currentTimeMillis();
}
public String toJsonString() {
try {
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);
default void onRateLimit(RuleEngineException e) {
onFailure(e);
};
/**
* 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.

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 org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.AbstractRateLimitException;
/**
* Created by ashvayka on 22.10.18.
*/
public class TbRateLimitsException extends RuntimeException {
public class TbRateLimitsException extends AbstractRateLimitException {
@Getter
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
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
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
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;
@Value("${zk.zk_dir}")
private String zkDir;
@Value("${zk.recalculate_delay:60000}")
@Value("${zk.recalculate_delay:0}")
private Long recalculateDelay;
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.TbQueueProducer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -53,10 +56,14 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
private final Set<TopicPartitionInfo> topics;
@Getter
private final String clientId;
@Builder
private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) {
Properties props = settings.toProducerProps();
this.clientId = Objects.requireNonNull(clientId, "Kafka producer client.id is null");
if (!StringUtils.isEmpty(clientId)) {
props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
}
@ -72,6 +79,22 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
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
public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) {
try {
@ -79,7 +102,10 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
String key = msg.getKey().toString();
byte[] data = msg.getData();
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);
producer.send(record, (metadata, exception) -> {
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)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
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",
ExecutionContext.class, String.class)));
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 {
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) {
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.ExecutionHashMap;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -348,6 +349,24 @@ public class TbUtilsTest {
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) {
List<Byte> result = new ArrayList<>(data.length);
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.DeviceProfile;
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.security.DeviceTokenCredentials;
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();
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()));
} else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) {
} else if (size == FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) {
if (uriPath.contains(DataConstants.PROVISION)) {
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;
}
/**
* For this hot method, the partial index `idx_notification_recipient_id_unread` was introduced since 3.5.2
* */
@Override
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) {
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) {
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) {

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_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;
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.ListeningExecutorService;
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.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
@ -74,8 +76,18 @@ import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevi
public class MqttClientTest extends AbstractContainerTest {
private Device device;
AbstractListeningExecutor handlerExecutor;
@BeforeMethod
public void setUp() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
}
@ -83,6 +95,9 @@ public class MqttClientTest extends AbstractContainerTest {
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
if (handlerExecutor != null) {
handlerExecutor.destroy();
}
}
@Test
public void telemetryUpload() throws Exception {
@ -461,11 +476,16 @@ public class MqttClientTest extends AbstractContainerTest {
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 {
MqttClientConfig clientConfig = new MqttClientConfig();
clientConfig.setOwnerId(getOwnerId());
clientConfig.setClientId("MQTT client from test");
clientConfig.setUsername(username);
MqttClient mqttClient = MqttClient.create(clientConfig, listener);
MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor);
mqttClient.connect("localhost", 1883).get();
return mqttClient;
}
@ -479,9 +499,10 @@ public class MqttClientTest extends AbstractContainerTest {
}
@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);
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;
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.ListeningExecutorService;
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.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient;
@ -76,8 +78,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
private MqttMessageListener listener;
private JsonParser jsonParser = new JsonParser();
AbstractListeningExecutor handlerExecutor;
@BeforeMethod
public void createGateway() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
testRestClient.login("tenant@thingsboard.org", "tenant");
gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype());
DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId());
@ -94,6 +106,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
this.listener = null;
this.mqttClient = null;
this.createdDevice = null;
if (handlerExecutor != null) {
handlerExecutor.destroy();
}
}
@Test
@ -403,11 +418,16 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
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 {
MqttClientConfig clientConfig = new MqttClientConfig();
clientConfig.setOwnerId(getOwnerId());
clientConfig.setClientId("MQTT client from test");
clientConfig.setUsername(deviceCredentials.getCredentialsId());
MqttClient mqttClient = MqttClient.create(clientConfig, listener);
MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor);
mqttClient.connect("localhost", 1883).get();
return mqttClient;
}
@ -421,9 +441,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
}
@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);
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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
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)

4
netty-mqtt/pom.xml

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

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

@ -16,6 +16,10 @@
package org.thingsboard.mqtt;
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.ChannelHandlerContext;
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.MqttUnsubAckMessage;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage> {
@ -117,27 +125,48 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
super.channelInactive(ctx);
}
private void invokeHandlersForIncomingPublish(MqttPublishMessage message) {
boolean handlerInvoked = false;
for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) {
if (subscription.matches(message.variableHeader().topicName())) {
if (subscription.isOnce() && subscription.isCalled()) {
continue;
}
message.payload().markReaderIndex();
subscription.setCalled(true);
subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
if (subscription.isOnce()) {
this.client.off(subscription.getTopic(), subscription.getHandler());
ListenableFuture<Void> invokeHandlersForIncomingPublish(MqttPublishMessage message) {
var future = Futures.immediateVoidFuture();
var handlerInvoked = new AtomicBoolean();
try {
for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) {
if (subscription.matches(message.variableHeader().topicName())) {
future = Futures.transform(future, x -> {
if (subscription.isOnce() && subscription.isCalled()) {
return null;
}
message.payload().markReaderIndex();
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) {
client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload());
}
message.payload().release();
return future;
}
private void handleConack(Channel channel, MqttConnAckMessage message) {
@ -204,11 +233,13 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
break;
case AT_LEAST_ONCE:
invokeHandlersForIncomingPublish(message);
var future = invokeHandlersForIncomingPublish(message);
if (message.variableHeader().packetId() != -1) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader));
future.addListener(() -> {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader));
}, MoreExecutors.directExecutor());
}
break;
@ -263,14 +294,20 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
}
private void handlePubrel(Channel channel, MqttMessage message) {
var future = Futures.immediateVoidFuture();
if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) {
MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId());
future = invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
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);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader));
future.addListener(() -> {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader));
}, MoreExecutors.directExecutor());
}
private void handlePubcomp(MqttMessage message) {
@ -281,4 +318,21 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
pendingPublish.getPayload().release();
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.handler.codec.mqtt.MqttQoS;
import io.netty.util.concurrent.Future;
import org.thingsboard.common.util.ListeningExecutor;
public interface MqttClient {
@ -71,6 +72,8 @@ public interface MqttClient {
*/
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
*
@ -180,8 +183,8 @@ public interface MqttClient {
* @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
*/
static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler){
return new MqttClientImpl(config, defaultHandler);
static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler, ListeningExecutor handlerExecutor){
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.handler.codec.mqtt.MqttVersion;
import io.netty.handler.ssl.SslContext;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -26,10 +28,12 @@ import java.util.Random;
@SuppressWarnings({"WeakerAccess", "unused"})
public final class MqttClientConfig {
private final SslContext sslContext;
private final String randomClientId;
@Getter
@Setter
private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes
private String clientId;
private int timeoutSeconds = 60;
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.Promise;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ListeningExecutor;
import java.util.Collections;
import java.util.HashSet;
@ -88,13 +89,13 @@ final class MqttClientImpl implements MqttClient {
private int port;
private MqttClientCallback callback;
private final ListeningExecutor handlerExecutor;
/**
* Construct the MqttClientImpl with default config
*/
public MqttClientImpl(MqttHandler defaultHandler) {
this.clientConfig = new MqttClientConfig();
this.defaultHandler = defaultHandler;
public MqttClientImpl(MqttHandler defaultHandler, ListeningExecutor handlerExecutor) {
this(new MqttClientConfig(), defaultHandler, handlerExecutor);
}
/**
@ -103,9 +104,10 @@ final class MqttClientImpl implements MqttClient {
*
* @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.defaultHandler = defaultHandler;
this.handlerExecutor = handlerExecutor;
}
/**
@ -227,6 +229,11 @@ final class MqttClientImpl implements MqttClient {
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
*

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

@ -15,9 +15,10 @@
*/
package org.thingsboard.mqtt;
import com.google.common.util.concurrent.ListenableFuture;
import io.netty.buffer.ByteBuf;
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 boolean called;
private volatile boolean called;
MqttSubscription(String topic, MqttHandler handler, boolean once) {
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.Before;
import org.junit.Test;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttConnectResult;
@ -49,8 +50,18 @@ public class MqttIntegrationTest {
MqttClient mqttClient;
AbstractListeningExecutor handlerExecutor;
@Before
public void init() throws Exception {
this.handlerExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 4;
}
};
handlerExecutor.init();
this.eventLoopGroup = new NioEventLoopGroup();
this.mqttServer = new MqttServer();
@ -68,6 +79,9 @@ public class MqttIntegrationTest {
if (this.eventLoopGroup != null) {
this.eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
}
if (this.handlerExecutor != null) {
this.handlerExecutor.destroy();
}
}
@Test
@ -108,9 +122,10 @@ public class MqttIntegrationTest {
private MqttClient initClient() throws Exception {
MqttClientConfig config = new MqttClientConfig();
config.setOwnerId("MqttIntegrationTest");
config.setTimeoutSeconds(KEEPALIVE_TIMEOUT_SECONDS);
config.setReconnectDelay(RECONNECT_DELAY_SECONDS);
MqttClient client = MqttClient.create(config, null);
MqttClient client = MqttClient.create(config, null, handlerExecutor);
client.setEventLoop(this.eventLoopGroup);
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,
Long startTs,
Long endTs,
boolean rewriteLatestIfDeleted) {
boolean rewriteLatestIfDeleted,
boolean deleteLatest) {
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString());
@ -2373,17 +2374,34 @@ public class RestClient implements Closeable {
params.put("startTs", startTs.toString());
params.put("endTs", endTs.toString());
params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted));
params.put("deleteLatest", String.valueOf(deleteLatest));
return restTemplate
.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,
HttpEntity.EMPTY,
Object.class,
params)
.getStatusCode()
.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) {

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.RuleNode;
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.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -50,7 +49,7 @@ public class TbMsgTypeSwitchNode implements TbNode {
@Override
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
public TbMathNodeConfiguration defaultConfiguration() {
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
configuration.setOperation(TbRuleNodeMathFunctionType.ADD);
configuration.setArguments(Arrays.asList(new TbMathArgument("x", TbMathArgumentType.CONSTANT, "2"), new TbMathArgument("y", TbMathArgumentType.CONSTANT, "2")));
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null));
configuration.setOperation(TbRuleNodeMathFunctionType.CUSTOM);
configuration.setCustomFunction("(t - 32) / 1.8");
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;
}
}

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 {
MqttClientConfig config = new MqttClientConfig(getSslContext());
config.setOwnerId(getOwnerId(ctx));
if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId());
@ -114,7 +119,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());
prepareMqttClientConfig(config);
MqttClient client = MqttClient.create(config, null);
MqttClient client = MqttClient.create(config, null, ctx.getExternalCallExecutor());
client.setEventLoop(ctx.getSharedEventLoop());
Future<MqttConnectResult> connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort());
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
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);
assertThat(msg).isNotNull();
assertThat(msg.getType()).isNotNull();
assertThat(msg.getType()).isEqualTo(msg.getInternalType().name());
assertThat(msg).isSameAs(tbMsgList.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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache:
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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache:
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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache:
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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache:
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}"
# Name of the directory in zookeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}"
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
cache:
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));
}
public ackAlarm(alarmId: string, config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config));
public ackAlarm(alarmId: string, config?: RequestConfig): Observable<AlarmInfo> {
return this.http.post<AlarmInfo>(`/api/alarm/${alarmId}/ack`, null, defaultHttpOptionsFromConfig(config));
}
public clearAlarm(alarmId: string, config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config));
public clearAlarm(alarmId: string, config?: RequestConfig): Observable<AlarmInfo> {
return this.http.post<AlarmInfo>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config));
}
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));
}
public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config));
public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<boolean> {
return this.http.delete<boolean>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config));
}
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,
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(',');
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +
`?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`;
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`;
if (isDefinedAndNotNull(deleteAllDataForKeys)) {
url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`;
}
if (isDefinedAndNotNull(rewriteLatestIfDeleted)) {
url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`;
}
if (isDefinedAndNotNull(deleteLatest)) {
url += `&deleteLatest=${deleteLatest}`;
}
if (isDefinedAndNotNull(startTs)) {
url += `&startTs=${startTs}`;
}
@ -103,7 +112,8 @@ export class AttributeService {
});
let deleteEntityTimeseriesObservable: Observable<any>;
if (deleteTimeseries.length) {
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config);
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true,
null, null, false, true, config);
} else {
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 { MatDialog } from '@angular/material/dialog';
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 { EntityId } from '@shared/models/id/entity-id';
import {
AlarmInfo,
AlarmQuery,
AlarmQueryV2,
AlarmSearchStatus,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmsMode,
AlarmStatus,
alarmStatusTranslations
} from '@app/shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service';
@ -46,7 +46,7 @@ import {
AlarmDetailsDialogComponent,
AlarmDetailsDialogData
} 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 { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@ -93,7 +93,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.pageMode = pageMode;
this.defaultTimewindowInterval = forAllTimeInterval();
this.detailsPanelEnabled = false;
this.selectionEnabled = false;
this.selectionEnabled = true;
this.searchEnabled = true;
this.addEnabled = false;
this.entitiesDeleteEnabled = false;
@ -155,6 +155,27 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
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>> {
@ -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} }}
</span>
<span fxFlex></span>
<button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"
<button [fxShow]="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
class="button-widget-action"
mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteAttributes($event)">
(click)="deleteTelemetry($event)">
<mat-icon>delete</mat-icon>
</button>
<button mat-raised-button color="accent"
@ -198,6 +198,10 @@
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</span>
<button mat-icon-button *ngIf="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY"
(click)="deleteTimeseries($event, attribute)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>

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

@ -49,6 +49,7 @@ import {
LatestTelemetry,
TelemetryType,
telemetryTypeTranslations,
TimeseriesDeleteStrategy,
toTelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
@ -82,10 +83,15 @@ import {
AddWidgetToDashboardDialogComponent,
AddWidgetToDashboardDialogData
} 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 { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer';
import {
DELETE_TIMESERIES_PANEL_DATA,
DeleteTimeseriesPanelComponent,
DeleteTimeseriesPanelData
} from '@home/components/attribute/delete-timeseries-panel.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) {
if ($event) {
$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() {
this.mode = 'widget';
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
} from '@home/components/event/event-content-dialog.component';
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 { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
@ -92,6 +93,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.loadDataOnInit = false;
this.tableTitle = '';
this.useTimePageLink = true;
this.defaultTimewindowInterval = historyInterval(MINUTE * 15);
this.detailsPanelEnabled = false;
this.selectionEnabled = false;
this.searchEnabled = false;
@ -179,7 +181,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
updateColumns(updateTableColumns: boolean = false): void {
this.columns = [];
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',
(entity) => entity.body.server, entity => ({}), false));
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';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
@NgModule({
declarations:
@ -205,6 +206,7 @@ import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/ba
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
DeleteTimeseriesPanelComponent,
AliasesEntitySelectPanelComponent,
AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent,

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

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

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

@ -14,43 +14,15 @@
* limitations under the License.
*/
:host {
.tb-relation-filters {
max-width: calc(100vw - 48px);
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;
}
}
}
.flex-50 {
flex: 1 1 50%;
}
.any-filter{
margin: 10px 0 20px;
}
.actions-header {
width: 40px
}
.add-button {
margin: 5px 0px 15px;
}
.entity-type-list {
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.
-->
<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-select formControlName="latest">
<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-title">{{ panelTitle }}</div>
<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 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>
@ -32,7 +32,7 @@
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
<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;">
<tb-data-key-row fxFlex
[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) => {
if (res) {
if (res) {
const tasks: Observable<void>[] = [];
const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) {
tasks.push(this.alarmService.ackAlarm(alarmId));
}
@ -959,7 +959,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
).subscribe((res) => {
if (res) {
if (res) {
const tasks: Observable<void>[] = [];
const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) {
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;
max-width: 750px !important;
.mat-mdc-dialog-content {
padding: 8px;
}
@media #{$mat-lt-md} {
min-width: 100% !important;
}

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

@ -27,10 +27,7 @@
display: flex;
flex-direction: column;
gap: 8px;
@media #{$mat-gt-sm} {
margin-bottom: 24px;
}
margin-bottom: 22px;
}
@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.
-->
<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-chip-grid #chipList formControlName="entitySubtypeList">
<mat-chip-row
@ -38,13 +39,6 @@
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="chipAdd($event)">
</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"
class="tb-autocomplete"
(optionSelected)="selected($event)"
@ -58,6 +52,9 @@
</span>
</mat-option>
</mat-autocomplete>
<div matSuffix>
<ng-content select="[additionalButtons]"></ng-content>
</div>
<mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')">
{{ subtypeListEmptyText | translate }}
</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 { 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 { map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -23,14 +23,15 @@ import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntitySubtype, EntityType } from '@shared/models/entity-type.models';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipGrid } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips';
import { AssetService } from '@core/http/asset.service';
import { DeviceService } from '@core/http/device.service';
import { EdgeService } from '@core/http/edge.service';
import { EntityViewService } from '@core/http/entity-view.service';
import { BroadcastService } from '@core/services/broadcast.service';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { coerceBoolean } from '@shared/decorators/coercion';
import { FloatLabelType } from '@angular/material/form-field';
@Component({
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 {
entitySubtypeListFormGroup: UntypedFormGroup;
entitySubtypeListFormGroup: FormGroup;
modelValue: Array<string> | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input() label: string;
@Input()
@coerceBoolean()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
if (this.requiredValue !== value) {
this.requiredValue = value;
this.updateValidators();
}
}
@Input()
floatLabel: FloatLabelType = 'auto';
@Input()
label: string;
@Input()
disabled: boolean;
@Input()
entityType: EntityType;
@Input()
emptyInputPlaceholder: string;
@Input()
filledInputPlaceholder: string;
@ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef<HTMLInputElement>;
@ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: true}) chipList: MatChipGrid;
@ -102,13 +114,14 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private deviceService: DeviceService,
private edgeService: EdgeService,
private entityViewService: EntityViewService,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
entitySubtype: [null]
});
}
updateValidators() {
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []);
this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity();
@ -122,7 +135,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
}
ngOnInit() {
switch (this.entityType) {
case EntityType.ASSET:
this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type')
@ -166,6 +178,13 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
break;
}
if (this.emptyInputPlaceholder) {
this.placeholder = this.emptyInputPlaceholder;
}
if (this.filledInputPlaceholder) {
this.secondaryPlaceholder = this.filledInputPlaceholder;
}
this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges
.pipe(
map(value => value ? value : ''),
@ -225,13 +244,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
}
this.clear('');
}
clearChipGrid() {
this.entitySubtypeList = [];
this.modelValue = null;
this.entitySubtypeListFormGroup.get('entitySubtypeList').patchValue([], {emitEvent: true});
}
remove(entitySubtype: string) {
const index = this.entitySubtypeList.indexOf(entitySubtype);
if (index >= 0) {

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

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

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

@ -30,19 +30,21 @@
</button>
</div>
</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
(click)="toggleHelp()">
<button mat-button
color="primary"
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>
<ng-container *ngIf="triggerSafeHtml">
<span [style]="triggerStyle" [innerHTML]="triggerSafeHtml"></span>
</ng-container>
<mat-icon *ngIf="!popoverVisible || popoverReady" class="tb-mat-16">open_in_new</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" mode="indeterminate" diameter="16" strokeWidth="2"></mat-spinner>
<mat-icon *ngIf="(!popoverVisible || popoverReady)"
[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>
</button>
</div>

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

@ -17,6 +17,9 @@
width: initial;
display: inline-block;
vertical-align: middle;
&.hint-button {
line-height: 1;
}
}
.tb-help-popup-button {
@ -65,4 +68,19 @@
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 { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@ -62,6 +63,11 @@ export class HelpPopupComponent implements OnChanges, OnDestroy {
popoverVisible = false;
popoverReady = true;
@Input()
@coerceBoolean()
hintMode = false;
triggerSafeHtml: SafeHtml = null;
textMode = false;

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

@ -15,7 +15,8 @@
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>
<input matInput type="text"
#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 { coerceBooleanProperty } from '@angular/cdk/coercion';
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({
selector: 'tb-relation-type-autocomplete',
@ -42,19 +43,22 @@ export class RelationTypeAutocompleteComponent implements ControlValueAccessor,
modelValue: string | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
label: string;
@Input() label: string;
@Input()
@coerceArray()
additionalClasses: Array<string>;
@Input() floatLabel: FloatLabelType = 'auto';
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
floatLabel: FloatLabelType = 'auto';
@Input()
@coerceBoolean()
required: boolean;
@Input()
disabled: boolean;

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

@ -54,7 +54,15 @@
{{ 'common.not-found' | translate }}
</mat-option>
</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')">
{{ requiredText }}
</mat-error>

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

@ -61,6 +61,13 @@ export enum TelemetryFeature {
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 const toTelemetryType = (val: string): TelemetryType => {
@ -73,7 +80,7 @@ export const toTelemetryType = (val: string): TelemetryType => {
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.SERVER_SCOPE, 'attribute.scope-server'],
[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 {
lastUpdateTs?: number;
key: string;

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

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

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

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

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

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

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

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

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

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

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

@ -512,6 +512,7 @@
"severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge",
"clear": "Clear",
"delete": "Delete",
"search": "Search alarms",
"selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} } selected",
"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-alarm-title": "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-text": "Are you sure you want to clear { count, plural, =1 {1 alarm} other {# alarms} }?",
"clear-alarm-title": "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-filter-title": "Alarm Filter",
"assigned": "Assigned",
@ -699,6 +704,7 @@
"latest-telemetry": "Latest telemetry",
"no-latest-telemetry": "No latest telemetry",
"attributes-scope": "Entity attributes scope",
"scope-telemetry": "Telemetry",
"scope-latest-telemetry": "Latest telemetry",
"scope-client": "Client attributes",
"scope-server": "Server attributes",
@ -726,7 +732,19 @@
"no-telemetry-text": "No telemetry found",
"copy-key": "Copy key",
"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-features": "API features",
@ -2035,6 +2053,7 @@
"entity-types": "Entity types",
"entity-type-list": "Entity type list",
"any-entity": "Any entity",
"add-entity-type": "Add entity type",
"enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' 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-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-filter": "Remove filter",
"add-relation-filter": "Add relation filter",
"any-relation": "Any relation",
"relation-filters": "Relation filters",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save