diff --git a/application/pom.xml b/application/pom.xml
index 4114f98759..99c460bb45 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
application
diff --git a/application/src/main/data/json/system/widget_types/html_container.json b/application/src/main/data/json/system/widget_types/html_container.json
index 25b9e5ac5d..a27996593b 100644
--- a/application/src/main/data/json/system/widget_types/html_container.json
+++ b/application/src/main/data/json/system/widget_types/html_container.json
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n",
+ "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'javaScript': {\n name: 'JavaScript',\n multiple: true\n }\n };\n}",
"settingsDirective": "tb-html-container-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-html-container-basic-config",
diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
index 591228e341..41e8949a71 100644
--- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
@@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
+import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
@@ -52,6 +53,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
@Hidden
@@ -76,9 +78,14 @@ public class SystemInfoController extends BaseController {
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
+ @Value("${sql.entity_data_query_nulls_order_strategy:default}")
+ private String nullsOrderStrategy;
+
@Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}")
private String iotHubBaseUrl;
+ private static final Set ACCEPTED_NULLS_ORDER_STRATEGIES = Set.of("default", "nulls_first", "nulls_last");
+
@Autowired(required = false)
private BuildProperties buildProperties;
@@ -94,6 +101,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private TrendzSettingsService trendzSettingsService;
+ @Autowired
+ private EdqsService edqsService;
+
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@@ -153,6 +163,8 @@ public class SystemInfoController extends BaseController {
}
systemParams.setUserSettings(userSettingsNode);
systemParams.setMaxDatapointsLimit(maxDatapointsLimit);
+ systemParams.setNullsOrderStrategy(ACCEPTED_NULLS_ORDER_STRATEGIES.contains(nullsOrderStrategy) ? nullsOrderStrategy : "default");
+ systemParams.setEdqsEnabled(edqsService.isApiEnabled());
if (!currentUser.isSystemAdmin()) {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
diff --git a/application/src/test/java/org/thingsboard/server/client/ClientDocsExampleTest.java b/application/src/test/java/org/thingsboard/server/client/ClientDocsExampleTest.java
new file mode 100644
index 0000000000..e75f3b29c0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/client/ClientDocsExampleTest.java
@@ -0,0 +1,318 @@
+/**
+ * Copyright © 2016-2026 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.client;
+
+import org.junit.Test;
+import org.thingsboard.client.ApiException;
+import org.thingsboard.client.ThingsboardClient;
+import org.thingsboard.client.model.ApiKeyInfo;
+import org.thingsboard.client.model.Asset;
+import org.thingsboard.client.model.AttributeData;
+import org.thingsboard.client.model.BooleanFilterPredicate;
+import org.thingsboard.client.model.BooleanOperation;
+import org.thingsboard.client.model.Device;
+import org.thingsboard.client.model.EntityCountQuery;
+import org.thingsboard.client.model.EntityKey;
+import org.thingsboard.client.model.EntityKeyType;
+import org.thingsboard.client.model.EntityKeyValueType;
+import org.thingsboard.client.model.EntityType;
+import org.thingsboard.client.model.EntityTypeFilter;
+import org.thingsboard.client.model.FilterPredicateValueBoolean;
+import org.thingsboard.client.model.KeyFilter;
+import org.thingsboard.client.model.PageDataDevice;
+import org.thingsboard.client.model.TsData;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Mirrors every code snippet from the Java client documentation page
+ * ({@code /docs/reference/java-client/}, CE edition). Each snippet appears
+ * character-for-character with two allowances:
+ *
+ * - placeholder values ({@code "{BASE_URL}"}, {@code "YOUR_API_KEY_VALUE"},
+ * {@code "YOUR_DEVICE_ID"}, {@code "YOUR_ASSET_ID"},
+ * {@code "nonexistent-id"}, {@code "tenant@thingsboard.org"},
+ * {@code "tenant"}) are swapped for real test values;
+ * - {@code System.out.println} / {@code System.out.printf} calls inside the
+ * snippet are replaced with equivalent JUnit assertions, so the test
+ * actually verifies behavior instead of only compilation.
+ *
+ * Setup code that pre-creates entities required by a snippet and post-snippet
+ * verifications stay outside the snippet block.
+ */
+@DaoSqlTest
+public class ClientDocsExampleTest extends AbstractApiClientTest {
+
+ // /docs/reference/java-client/#quickstart
+ @Test
+ public void testQuickstart() throws Exception {
+ // setup: real API key for the snippet's "YOUR_API_KEY_VALUE" placeholder
+ ApiKeyInfo keyRequest = new ApiKeyInfo();
+ keyRequest.setDescription("ClientDocsExampleTest");
+ keyRequest.setUserId(clientTenantAdmin.getId());
+ keyRequest.setEnabled(true);
+ String apiKeyValue = this.client.saveApiKey(keyRequest).getValue();
+
+ // === doc snippet ===
+ ThingsboardClient client = ThingsboardClient.builder()
+ .url(getBaseUrl())
+ .apiKey(apiKeyValue)
+ .build();
+
+ Device newDevice = new Device();
+ newDevice.setName("Quickstart Device");
+ newDevice.setType("default");
+ Device savedDevice = client.saveDevice(newDevice, null, null, null, null);
+
+ String deviceId = savedDevice.getId().getId().toString();
+ client.saveEntityTelemetry("DEVICE", deviceId, "ANY", """
+ {"temperature": 22.4}
+ """);
+ assertEquals("Quickstart Device", savedDevice.getName());
+
+ client.deleteDevice(deviceId);
+
+ // post-snippet verification: the device is gone after deletion
+ assertReturns404(() -> client.getDeviceById(deviceId));
+ }
+
+ // /docs/reference/java-client/#api-key-recommended
+ @Test
+ public void testAuthenticationViaApiKey() throws Exception {
+ // setup: real API key for the snippet's "YOUR_API_KEY_VALUE" placeholder
+ ApiKeyInfo keyRequest = new ApiKeyInfo();
+ keyRequest.setDescription("ClientDocsExampleTest");
+ keyRequest.setUserId(clientTenantAdmin.getId());
+ keyRequest.setEnabled(true);
+ String apiKeyValue = this.client.saveApiKey(keyRequest).getValue();
+
+ // === doc snippet ===
+ String url = getBaseUrl();
+ String apiKey = apiKeyValue;
+ ThingsboardClient client = ThingsboardClient.builder()
+ .url(url)
+ .apiKey(apiKey)
+ .build();
+
+ assertEquals(TENANT_ADMIN_USERNAME, client.getUser().getEmail());
+ }
+
+ // /docs/reference/java-client/#username-and-password-jwt
+ @Test
+ public void testAuthenticationViaCredentials() throws Exception {
+ // === doc snippet ===
+ String url = getBaseUrl();
+ ThingsboardClient client = ThingsboardClient.builder()
+ .url(url)
+ .credentials(TENANT_ADMIN_USERNAME, TEST_PASSWORD)
+ .build();
+
+ assertEquals(TENANT_ADMIN_USERNAME, client.getUser().getEmail());
+ }
+
+ // /docs/reference/java-client/#rate-limit-handling
+ @Test
+ public void testRateLimitHandlingBuilderOptions() throws Exception {
+ // setup: real url + api key that the snippet references as locals
+ String url = getBaseUrl();
+ ApiKeyInfo keyRequest = new ApiKeyInfo();
+ keyRequest.setDescription("ClientDocsExampleTest");
+ keyRequest.setUserId(clientTenantAdmin.getId());
+ keyRequest.setEnabled(true);
+ String apiKey = this.client.saveApiKey(keyRequest).getValue();
+
+ // === doc snippet ===
+ ThingsboardClient client = ThingsboardClient.builder()
+ .url(url)
+ .apiKey(apiKey)
+ .maxRetries(3) // default 3
+ .initialRetryDelayMs(1000) // default 1 s
+ .maxRetryDelayMs(30_000) // default 30 s
+ .build();
+
+ // post-snippet verification: the tuned client is actually usable
+ assertEquals(TENANT_ADMIN_USERNAME, client.getUser().getEmail());
+ }
+
+ // /docs/reference/java-client/#working-with-entities
+ @Test
+ public void testWorkingWithEntities() throws Exception {
+ // === doc snippet ===
+ Device newDevice = new Device();
+ newDevice.setName("Test Device");
+ newDevice.setType("default");
+ Device savedDevice = client.saveDevice(newDevice, null, null, null, null);
+
+ String deviceId = savedDevice.getId().getId().toString();
+ Device fetched = client.getDeviceById(deviceId);
+ assertEquals("Test Device", fetched.getName());
+
+ client.deleteDevice(deviceId);
+
+ // post-snippet verification: the device is gone after deletion
+ assertReturns404(() -> client.getDeviceById(deviceId));
+ }
+
+ // /docs/reference/java-client/#push-telemetry
+ @Test
+ public void testPushTelemetry() throws Exception {
+ // setup: create a real device whose id replaces "YOUR_DEVICE_ID"
+ Device setup = new Device();
+ setup.setName("Telemetry Setup Device");
+ setup.setType("default");
+ String realDeviceId = client.saveDevice(setup, null, null, null, null)
+ .getId().getId().toString();
+
+ // === doc snippet ===
+ String deviceId = realDeviceId;
+ String body = """
+ {"temperature": 26.5, "humidity": 87}
+ """;
+ client.saveEntityTelemetry("DEVICE", deviceId, "ANY", body);
+
+ // post-snippet verification: telemetry was actually persisted
+ Map> latest =
+ client.getLatestTimeseries("DEVICE", deviceId, "temperature,humidity", false, null);
+ assertEquals("26.5", latest.get("temperature").get(0).getValue().toString());
+ assertEquals("87", latest.get("humidity").get(0).getValue().toString());
+ }
+
+ // /docs/reference/java-client/#read-and-write-attributes-read-modify-write
+ @Test
+ public void testReadModifyWriteAttributes() throws Exception {
+ // setup: create a real asset whose id replaces "YOUR_ASSET_ID"
+ Asset setupAsset = new Asset();
+ setupAsset.setName("Counter Setup Asset");
+ setupAsset.setType("building");
+ String realAssetId = client.saveAsset(setupAsset, null, null, null)
+ .getId().getId().toString();
+
+ // === doc snippet ===
+ String assetId = realAssetId;
+
+ List attrs = client.getAttributesByScope(
+ "ASSET", assetId, "SERVER_SCOPE", "deviceCount", null);
+
+ // getValue() returns Object — JSON numbers come back as Number subclasses
+ long current = attrs.isEmpty() ? 0L : ((Number) attrs.get(0).getValue()).longValue();
+ long updated = current + 1;
+
+ client.saveEntityAttributesV2("ASSET", assetId, "SERVER_SCOPE",
+ "{\"deviceCount\": %d}".formatted(updated));
+
+ // post-snippet verification: the increment was actually persisted
+ List after = client.getAttributesByScope(
+ "ASSET", assetId, "SERVER_SCOPE", "deviceCount", null);
+ assertEquals(1, after.size());
+ assertEquals(updated, ((Number) after.get(0).getValue()).longValue());
+ }
+
+ // /docs/reference/java-client/#paginated-tenant-list
+ @Test
+ public void testPaginatedTenantList() throws Exception {
+ // setup: populate the tenant with a few devices so the iteration has something to walk
+ int expectedDeviceCount = 5;
+ for (int i = 0; i < expectedDeviceCount; i++) {
+ Device d = new Device();
+ d.setName("Page Setup Device " + i);
+ d.setType("default");
+ client.saveDevice(d, null, null, null, null);
+ }
+
+ // === doc snippet ===
+ int page = 0; // pages are zero-indexed
+ PageDataDevice devices;
+ do {
+ devices = client.getTenantDevices(100, page, null, null, null, null);
+ devices.getData().forEach(d -> assertEquals("default", d.getType()));
+ page++;
+ } while (devices.getHasNext());
+
+ // post-snippet verification: pagination terminated and reached every device
+ assertEquals((long) expectedDeviceCount, devices.getTotalElements().longValue());
+ }
+
+ // /docs/reference/java-client/#filtered-query-with-entity-data-query-api
+ @Test
+ public void testEntityDataQueryCountFiltered() throws Exception {
+ // setup: create a mix of active and inactive devices for the count query
+ Device active1 = client.saveDevice(
+ new Device().name("Active_1").type("default"),
+ null, null, null, null);
+ Device active2 = client.saveDevice(
+ new Device().name("Active_2").type("default"),
+ null, null, null, null);
+ client.saveDevice(
+ new Device().name("Inactive_1").type("default"),
+ null, null, null, null);
+ client.saveEntityAttributesV2("DEVICE", active1.getId().getId().toString(),
+ "SERVER_SCOPE", "{\"active\": true}");
+ client.saveEntityAttributesV2("DEVICE", active2.getId().getId().toString(),
+ "SERVER_SCOPE", "{\"active\": true}");
+
+ // === doc snippet ===
+ EntityTypeFilter typeFilter = new EntityTypeFilter();
+ typeFilter.setEntityType(EntityType.DEVICE);
+
+ EntityCountQuery totalQuery = new EntityCountQuery();
+ totalQuery.setEntityFilter(typeFilter);
+ assertEquals(3L, client.countEntitiesByQuery(totalQuery).longValue());
+
+ KeyFilter activeFilter = new KeyFilter();
+ activeFilter.setKey(new EntityKey().type(EntityKeyType.ATTRIBUTE).key("active"));
+ activeFilter.setValueType(EntityKeyValueType.BOOLEAN);
+ BooleanFilterPredicate predicate = new BooleanFilterPredicate();
+ predicate.setOperation(BooleanOperation.EQUAL);
+ predicate.setValue(new FilterPredicateValueBoolean().defaultValue(true));
+ activeFilter.setPredicate(predicate);
+
+ EntityCountQuery activeQuery = new EntityCountQuery();
+ activeQuery.setEntityFilter(typeFilter);
+ activeQuery.setKeyFilters(List.of(activeFilter));
+ assertEquals(2L, client.countEntitiesByQuery(activeQuery).longValue());
+ }
+
+ // /docs/reference/java-client/#error-handling
+ @Test
+ public void testErrorHandling404() {
+ // setup: a real (random) UUID that doesn't resolve, replacing "nonexistent-id";
+ // the flag captures whether the 404 branch ran so we can assert the snippet
+ // actually entered error handling (instead of silently completing).
+ String missingDeviceId = UUID.randomUUID().toString();
+ boolean[] caught404 = {false};
+
+ // === doc snippet ===
+ try {
+ Device device = client.getDeviceById(missingDeviceId);
+ } catch (ApiException e) {
+ if (e.getCode() == 404) {
+ caught404[0] = true;
+ } else {
+ fail("API error " + e.getCode() + ": " + e.getResponseBody());
+ }
+ }
+
+ // post-snippet verification: the snippet actually exercised the 404 branch
+ assertTrue("Expected ApiException with code 404", caught404[0]);
+ }
+}
diff --git a/common/actor/pom.xml b/common/actor/pom.xml
index 6d46c10198..b3f04d31a1 100644
--- a/common/actor/pom.xml
+++ b/common/actor/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/cache/pom.xml b/common/cache/pom.xml
index aa1438d97a..e95bb301b4 100644
--- a/common/cache/pom.xml
+++ b/common/cache/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml
index e8968e2a2c..63dc714000 100644
--- a/common/cluster-api/pom.xml
+++ b/common/cluster-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml
index 5314510d52..d713c6d065 100644
--- a/common/coap-server/pom.xml
+++ b/common/coap-server/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml
index 30eb360490..87b217e205 100644
--- a/common/dao-api/pom.xml
+++ b/common/dao-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 8579103da7..0e9d306b85 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
index 4983c879db..e148c413b5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
@@ -45,5 +45,7 @@ public class SystemParams {
long minAllowedAggregationIntervalInSecForCF;
long intermediateAggregationIntervalInSecForCF;
TrendzSettings trendzSettings;
+ String nullsOrderStrategy;
+ boolean edqsEnabled;
String iotHubBaseUrl;
}
diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml
index f74163642d..275d7762d5 100644
--- a/common/discovery-api/pom.xml
+++ b/common/discovery-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml
index 2bff3e0b18..647accd360 100644
--- a/common/edge-api/pom.xml
+++ b/common/edge-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto
index 311c345b7a..11de63a977 100644
--- a/common/edge-api/src/main/proto/edge.proto
+++ b/common/edge-api/src/main/proto/edge.proto
@@ -49,10 +49,12 @@ enum EdgeVersion {
V_4_2_2 = 4220;
V_4_2_2_1 = 4221;
V_4_2_2_2 = 4222;
+ V_4_2_2_3 = 4223;
V_4_3_0_1 = 15;
V_4_3_1 = 4310;
V_4_3_1_1 = 4311;
V_4_3_1_2 = 4312;
+ V_4_3_1_3 = 4313;
V_LATEST = 99999;
}
diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml
index 854ed34c54..769606efad 100644
--- a/common/edqs/pom.xml
+++ b/common/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 48a6471014..ee6d296262 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/pom.xml b/common/pom.xml
index 0759821ed4..c8f3935110 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
common
diff --git a/common/proto/pom.xml b/common/proto/pom.xml
index 433b84ceff..bee053364f 100644
--- a/common/proto/pom.xml
+++ b/common/proto/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/queue/pom.xml b/common/queue/pom.xml
index 16a508ee43..b24edd37d3 100644
--- a/common/queue/pom.xml
+++ b/common/queue/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/script/pom.xml b/common/script/pom.xml
index 278799abee..db477115cb 100644
--- a/common/script/pom.xml
+++ b/common/script/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml
index 4de100d423..8add60dd90 100644
--- a/common/script/remote-js-client/pom.xml
+++ b/common/script/remote-js-client/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
script
org.thingsboard.common.script
diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml
index 2455dc3423..45adffcceb 100644
--- a/common/script/script-api/pom.xml
+++ b/common/script/script-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
script
org.thingsboard.common.script
diff --git a/common/stats/pom.xml b/common/stats/pom.xml
index a89ee61a62..28930308ce 100644
--- a/common/stats/pom.xml
+++ b/common/stats/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml
index a44fd7334f..0608a957ab 100644
--- a/common/transport/coap/pom.xml
+++ b/common/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml
index 9e51de98b4..77949bb51e 100644
--- a/common/transport/http/pom.xml
+++ b/common/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml
index 9bf13bb33d..e71e0d9a9d 100644
--- a/common/transport/lwm2m/pom.xml
+++ b/common/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml
index 87ca63fcba..2533ec2b0f 100644
--- a/common/transport/mqtt/pom.xml
+++ b/common/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 12f0c87e2c..1629826eae 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml
index 2f4e2dae76..bb607f4b35 100644
--- a/common/transport/snmp/pom.xml
+++ b/common/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml
index d02278a5cc..6019b3b17c 100644
--- a/common/transport/transport-api/pom.xml
+++ b/common/transport/transport-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.common.transport
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java
index 63f2247aba..82b7981f8f 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java
@@ -27,7 +27,6 @@ import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
import org.thingsboard.server.queue.util.TbTransportComponent;
-import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -165,7 +164,6 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
static class CertificateWatcher {
private final List paths;
private final Runnable reloadCallback;
- private final Map lastModifiedMap;
private final Map lastChecksumMap;
private int consecutiveFailures;
private String failedCombinedChecksum;
@@ -173,60 +171,23 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
CertificateWatcher(List paths, Runnable reloadCallback) {
this.paths = paths;
this.reloadCallback = reloadCallback;
- this.lastModifiedMap = new HashMap<>();
this.lastChecksumMap = new HashMap<>();
for (Path path : paths) {
- lastModifiedMap.put(path, getLastModifiedTime(path));
lastChecksumMap.put(path, calculateChecksum(path));
}
this.consecutiveFailures = 0;
}
synchronized void checkAndReload(String name) {
- boolean anyModifiedChanged = false;
- for (Path path : paths) {
- long currentModified = getLastModifiedTime(path);
- Long lastModified = lastModifiedMap.getOrDefault(path, 0L);
- if (currentModified != lastModified) {
- anyModifiedChanged = true;
- break;
- }
- }
- if (!anyModifiedChanged) {
- return;
- }
-
- // Capture mtimes and checksums together before the callback runs.
- // Pairing a post-callback mtime with a pre-callback checksum would let a write-during-reload be missed on the next poll.
- Map currentModifiedTimes = new HashMap<>();
Map currentChecksums = new HashMap<>();
- StringBuilder combined = new StringBuilder();
for (Path path : paths) {
- currentModifiedTimes.put(path, getLastModifiedTime(path));
- String checksum = calculateChecksum(path);
- currentChecksums.put(path, checksum);
- if (!combined.isEmpty()) {
- combined.append("|");
- }
- combined.append(path).append("=").append(checksum);
+ currentChecksums.put(path, calculateChecksum(path));
}
- String combinedChecksum = combined.toString();
-
- // Build old combined checksum for comparison
- StringBuilder oldCombined = new StringBuilder();
- for (Path path : paths) {
- if (!oldCombined.isEmpty()) {
- oldCombined.append("|");
- }
- oldCombined.append(path).append("=").append(lastChecksumMap.getOrDefault(path, ""));
- }
- String oldCombinedChecksum = oldCombined.toString();
+ String combinedChecksum = combinedChecksum(currentChecksums);
+ String oldCombinedChecksum = combinedChecksum(lastChecksumMap);
if (combinedChecksum.equals(oldCombinedChecksum)) {
- // Content unchanged, just update modification times
- for (Path path : paths) {
- lastModifiedMap.put(path, currentModifiedTimes.get(path));
- }
+ // Content unchanged
return;
}
@@ -237,41 +198,34 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
}
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
- // Update modification times to avoid re-checking mtime and re-computing checksums every poll cycle
- for (Path path : paths) {
- lastModifiedMap.put(path, currentModifiedTimes.get(path));
- }
return;
}
try {
log.info("Certificate change detected for: {}. Triggering reload...", name);
reloadCallback.run();
- for (Path path : paths) {
- lastModifiedMap.put(path, currentModifiedTimes.get(path));
- lastChecksumMap.put(path, currentChecksums.get(path));
- }
+ lastChecksumMap.putAll(currentChecksums);
consecutiveFailures = 0;
failedCombinedChecksum = null;
} catch (Exception e) {
consecutiveFailures++;
failedCombinedChecksum = combinedChecksum;
- // Deliberately NOT updating the lastModifiedMap here, so the next poll cycle retries
- // (mtime mismatch passes the early gate, checksum matches failedCombinedChecksum).
+ // Deliberately NOT updating lastChecksumMap here, so the next poll cycle still sees a differing
+ // checksum, re-enters this method, and retries the same content.
log.error("Failed to reload certificate for {} (attempt {}/{}): {}",
name, consecutiveFailures, MAX_CONSECUTIVE_FAILURES, e.getMessage(), e);
}
}
- private long getLastModifiedTime(Path path) {
- try {
- if (!Files.exists(path)) {
- return 0;
+ private String combinedChecksum(Map checksums) {
+ StringBuilder combined = new StringBuilder();
+ for (Path path : paths) {
+ if (!combined.isEmpty()) {
+ combined.append("|");
}
- return Files.getLastModifiedTime(path).toMillis();
- } catch (IOException e) {
- return 0;
+ combined.append(path).append("=").append(checksums.getOrDefault(path, ""));
}
+ return combined.toString();
}
private String calculateChecksum(Path path) {
diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java
index 6f78eaefae..8894937177 100644
--- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java
+++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java
@@ -31,10 +31,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
public class CertificateReloadManagerTest {
@@ -59,11 +56,10 @@ public class CertificateReloadManagerTest {
}
}
- private void writeFileAndAwaitMtimeChange(Path path, String content, long baselineMtime) throws IOException {
+ private void writeFileAndBumpMtime(Path path, String content, long baselineMtime) throws IOException {
Files.writeString(path, content);
- await().atMost(2, SECONDS)
- .pollInterval(10, MILLISECONDS)
- .until(() -> Files.getLastModifiedTime(path).toMillis() != baselineMtime);
+ // Force a strictly newer mtime: back-to-back writes can share a millisecond, hiding the change from the watcher.
+ Files.setLastModifiedTime(path, FileTime.fromMillis(baselineMtime + 1000));
}
private long mtime(Path path) throws IOException {
@@ -77,7 +73,7 @@ public class CertificateReloadManagerTest {
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet);
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -85,7 +81,7 @@ public class CertificateReloadManagerTest {
}
@Test
- public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception {
+ public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() {
AtomicInteger reloadCount = new AtomicInteger(0);
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet);
@@ -147,7 +143,7 @@ public class CertificateReloadManagerTest {
certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadCount::incrementAndGet);
long baseline = mtime(keyFile);
- writeFileAndAwaitMtimeChange(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline);
+ writeFileAndBumpMtime(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -168,8 +164,8 @@ public class CertificateReloadManagerTest {
long baseline1 = mtime(certFile);
long baseline2 = mtime(cert2File);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1);
- writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1);
+ writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -191,8 +187,8 @@ public class CertificateReloadManagerTest {
long baseline1 = mtime(certFile);
long baseline2 = mtime(cert2File);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1);
- writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1);
+ writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -224,9 +220,7 @@ public class CertificateReloadManagerTest {
for (int i = 0; i < 5; i++) {
Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n");
}
- await().atMost(2, SECONDS)
- .pollInterval(10, MILLISECONDS)
- .until(() -> mtime(certFile) != baseline);
+ Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline + 1000));
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -242,7 +236,7 @@ public class CertificateReloadManagerTest {
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet);
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
@@ -272,7 +266,7 @@ public class CertificateReloadManagerTest {
certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet);
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, originalContent, baseline);
+ writeFileAndBumpMtime(certFile, originalContent, baseline);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -289,7 +283,7 @@ public class CertificateReloadManagerTest {
});
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
for (int i = 0; i < 15; i++) {
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -311,19 +305,41 @@ public class CertificateReloadManagerTest {
});
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
assertThat(reloadAttempts.get()).isEqualTo(1);
shouldFail.set(0);
long baseline2 = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
assertThat(reloadAttempts.get()).isEqualTo(2);
}
+ @Test
+ public void givenContentChangedButMtimeUnchanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception {
+ // Bug fingerprint: a cert-manager rotation that lands in the same wall-clock millisecond as the
+ // watcher's recorded baseline mtime. Files.getLastModifiedTime().toMillis() truncates to the ms,
+ // so the rotated content shares the baseline mtime and an mtime-only gate would never re-hash it.
+ AtomicInteger reloadCount = new AtomicInteger(0);
+
+ certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet);
+
+ long baseline = mtime(certFile);
+ Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nROTATED_SAME_MS\n-----END CERTIFICATE-----\n");
+ // Force the mtime back to the exact baseline millisecond — content changed, timestamp did not.
+ Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline));
+
+ // Sanity guard: the watcher observes a timestamp identical to its recorded baseline.
+ assertThat(mtime(certFile)).isEqualTo(baseline);
+
+ ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
+
+ assertThat(reloadCount.get()).isEqualTo(1);
+ }
+
@Test
public void givenCallbackHitMaxFailures_whenFileChangesToNewContent_thenShouldResetAndRetry() throws Exception {
AtomicInteger reloadAttempts = new AtomicInteger(0);
@@ -337,7 +353,7 @@ public class CertificateReloadManagerTest {
});
long baseline = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline);
for (int i = 0; i < 15; i++) {
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
@@ -346,7 +362,7 @@ public class CertificateReloadManagerTest {
shouldFail.set(0);
long baseline2 = mtime(certFile);
- writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2);
+ writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2);
ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates");
assertThat(reloadAttempts.get()).isEqualTo(11);
diff --git a/common/util/pom.xml b/common/util/pom.xml
index 1460119f02..ae6dd29420 100644
--- a/common/util/pom.xml
+++ b/common/util/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml
index 1ac78d2db2..d955a57537 100644
--- a/common/version-control/pom.xml
+++ b/common/version-control/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
common
org.thingsboard.common
diff --git a/dao/pom.xml b/dao/pom.xml
index 7b76c664d8..ae9f1a5998 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
dao
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java
index 12c4c62f94..a953e07197 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java
@@ -32,6 +32,10 @@ public class DashboardDataValidator extends DataValidator {
@Override
protected void validateCreate(TenantId tenantId, Dashboard data) {
+ validateMaxDashboardsPerTenant(tenantId);
+ }
+
+ public void validateMaxDashboardsPerTenant(TenantId tenantId) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.DASHBOARD);
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java
index 3029027f37..4b9038a894 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java
@@ -21,11 +21,19 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.usagerecord.ApiLimitService;
+import org.thingsboard.server.exception.EntitiesLimitExceededException;
import java.util.UUID;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify;
@@ -34,6 +42,8 @@ class DashboardDataValidatorTest {
@MockBean
TenantService tenantService;
+ @MockBean
+ ApiLimitService apiLimitService;
@SpyBean
DashboardDataValidator validator;
TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da"));
@@ -53,4 +63,25 @@ class DashboardDataValidatorTest {
verify(validator).validateString("Dashboard title", dashboard.getTitle());
}
+ @Test
+ void validateMaxDashboardsPerTenant_doesNotThrow_whenLimitNotReached() {
+ willReturn(true).given(apiLimitService).checkEntitiesLimit(tenantId, EntityType.DASHBOARD);
+
+ assertThatNoException().isThrownBy(() -> validator.validateMaxDashboardsPerTenant(tenantId));
+ }
+
+ @Test
+ void validateMaxDashboardsPerTenant_throwsEntitiesLimitExceeded_whenLimitReached() {
+ long limit = 5;
+ willReturn(false).given(apiLimitService).checkEntitiesLimit(tenantId, EntityType.DASHBOARD);
+ willReturn(limit).given(apiLimitService).getLimit(eq(tenantId), any());
+
+ assertThatThrownBy(() -> validator.validateMaxDashboardsPerTenant(tenantId))
+ .isInstanceOfSatisfying(EntitiesLimitExceededException.class, ex -> {
+ assertThat(ex.getTenantId()).isEqualTo(tenantId);
+ assertThat(ex.getEntityType()).isEqualTo(EntityType.DASHBOARD);
+ assertThat(ex.getLimit()).isEqualTo(limit);
+ });
+ }
+
}
diff --git a/edqs/pom.xml b/edqs/pom.xml
index 32c6df82f2..4d13edbc22 100644
--- a/edqs/pom.xml
+++ b/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
edqs
diff --git a/monitoring/pom.xml b/monitoring/pom.xml
index 5bc1fb5baa..2786e1b2c8 100644
--- a/monitoring/pom.xml
+++ b/monitoring/pom.xml
@@ -21,7 +21,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml
index b871933d9f..69a78c72bd 100644
--- a/msa/black-box-tests/pom.xml
+++ b/msa/black-box-tests/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml
index c9fcd7beb7..69cff7d0e6 100644
--- a/msa/edqs/pom.xml
+++ b/msa/edqs/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json
index 5e0c41fe85..0843ef2dbe 100644
--- a/msa/js-executor/package.json
+++ b/msa/js-executor/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
- "version": "4.3.1.2",
+ "version": "4.3.1.3",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",
diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml
index 4396f4fb96..f57e06f4f2 100644
--- a/msa/js-executor/pom.xml
+++ b/msa/js-executor/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml
index 2b4a0bd6da..9502552a52 100644
--- a/msa/monitoring/pom.xml
+++ b/msa/monitoring/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
diff --git a/msa/pom.xml b/msa/pom.xml
index a3494dfc3e..1498847ce3 100644
--- a/msa/pom.xml
+++ b/msa/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
msa
diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml
index e4ff8f4951..df53fda9c1 100644
--- a/msa/tb-node/pom.xml
+++ b/msa/tb-node/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml
index 6b14bb92fb..327668587f 100644
--- a/msa/tb/pom.xml
+++ b/msa/tb/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml
index 27a0210adf..c496c8c8d0 100644
--- a/msa/transport/coap/pom.xml
+++ b/msa/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml
index fb22d39da3..e15e74c8b7 100644
--- a/msa/transport/http/pom.xml
+++ b/msa/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml
index 916dc57712..7fe35560b3 100644
--- a/msa/transport/lwm2m/pom.xml
+++ b/msa/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml
index 1dc673ce47..dcceba1e43 100644
--- a/msa/transport/mqtt/pom.xml
+++ b/msa/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml
index a1e867fb05..cf2411cf3e 100644
--- a/msa/transport/pom.xml
+++ b/msa/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml
index 9ec7400a2c..b83b035b76 100644
--- a/msa/transport/snmp/pom.xml
+++ b/msa/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard.msa
transport
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
org.thingsboard.msa.transport
diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml
index 5f37f101f4..fc1de84048 100644
--- a/msa/vc-executor-docker/pom.xml
+++ b/msa/vc-executor-docker/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml
index f5beccabf6..0da1dbbbf4 100644
--- a/msa/vc-executor/pom.xml
+++ b/msa/vc-executor/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json
index ce18b0b063..6dc75cc9dd 100644
--- a/msa/web-ui/package.json
+++ b/msa/web-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
- "version": "4.3.1.2",
+ "version": "4.3.1.3",
"description": "ThingsBoard Web UI Microservice",
"main": "server.ts",
"bin": "server.js",
diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml
index a6b6fb6e8e..02ac55d4a8 100644
--- a/msa/web-ui/pom.xml
+++ b/msa/web-ui/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
msa
org.thingsboard.msa
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
index 9e06b2d975..92766bb38d 100644
--- a/netty-mqtt/pom.xml
+++ b/netty-mqtt/pom.xml
@@ -19,11 +19,11 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
netty-mqtt
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
jar
Netty MQTT Client
diff --git a/pom.xml b/pom.xml
index 306879f4fa..008bfe5a3a 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
pom
Thingsboard
@@ -70,7 +70,7 @@
3.5.13
3.18.0
42.7.11
- 4.1.133.Final
+ 4.1.134.Final
10.1.55
2.4.0-b180830.0359
0.12.5
diff --git a/rest-client/pom.xml b/rest-client/pom.xml
index fe159973cf..cc0e1009d5 100644
--- a/rest-client/pom.xml
+++ b/rest-client/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
rest-client
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
index 3368f77cdc..976dd842b5 100644
--- a/rule-engine/pom.xml
+++ b/rule-engine/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
rule-engine
diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml
index b4643ece39..85b305f1f2 100644
--- a/rule-engine/rule-engine-api/pom.xml
+++ b/rule-engine/rule-engine-api/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
rule-engine
org.thingsboard.rule-engine
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 1f25541bd9..4a3aaa5323 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -22,7 +22,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
rule-engine
org.thingsboard.rule-engine
diff --git a/tools/pom.xml b/tools/pom.xml
index 7a83b99e1f..12ea0159c1 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
tools
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 78e7ba7f71..74bc0becda 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 0fad7a5e63..db6459b3c9 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml
index 11d4c1f2db..61ae17fe7e 100644
--- a/transport/lwm2m/pom.xml
+++ b/transport/lwm2m/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 58b04e5bba..369cd33e8b 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
org.thingsboard.transport
diff --git a/transport/pom.xml b/transport/pom.xml
index 835f98f0ad..c4b690932f 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
transport
diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml
index 3158a9efaa..2f7981d9a0 100644
--- a/transport/snmp/pom.xml
+++ b/transport/snmp/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
transport
diff --git a/ui-ngx/package.json b/ui-ngx/package.json
index 200b489508..a17cf01152 100644
--- a/ui-ngx/package.json
+++ b/ui-ngx/package.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard",
- "version": "4.3.1.2",
+ "version": "4.3.1.3",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml
index 7c73c7bc33..61194d94bf 100644
--- a/ui-ngx/pom.xml
+++ b/ui-ngx/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 4.3.1.2-SNAPSHOT
+ 4.3.1.3-SNAPSHOT
thingsboard
org.thingsboard
diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts
index 12f5bc60d8..f7c719cc0c 100644
--- a/ui-ngx/src/app/core/api/widget-api.models.ts
+++ b/ui-ngx/src/app/core/api/widget-api.models.ts
@@ -119,6 +119,7 @@ export interface WidgetActionsApi {
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;
+ invokeAction: ($event: Event, actionName: string, additionalParams?: any) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef;
diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts
index 07a934e3bc..5603a21dd7 100644
--- a/ui-ngx/src/app/core/auth/auth.models.ts
+++ b/ui-ngx/src/app/core/auth/auth.models.ts
@@ -17,6 +17,7 @@
import { AuthUser, User } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { TrendzSettings } from '@shared/models/trendz-settings.models';
+import { NullsOrderStrategy } from '@shared/models/page/page-link';
export interface SysParamsState {
userTokenAccessEnabled: boolean;
@@ -40,6 +41,8 @@ export interface SysParamsState {
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
intermediateAggregationIntervalInSecForCF: number;
trendzSettings: TrendzSettings;
+ nullsOrderStrategy: NullsOrderStrategy;
+ edqsEnabled: boolean;
iotHubBaseUrl: string;
}
diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts
index 63ebc320fe..c0fe744f25 100644
--- a/ui-ngx/src/app/core/auth/auth.reducer.ts
+++ b/ui-ngx/src/app/core/auth/auth.reducer.ts
@@ -43,6 +43,8 @@ const emptyUserAuthState: AuthPayload = {
intermediateAggregationIntervalInSecForCF: 0,
userSettings: initialUserSettings,
trendzSettings: initialTrendzSettings,
+ nullsOrderStrategy: 'default',
+ edqsEnabled: false,
iotHubBaseUrl: ''
};
diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts
index 07a8c5b7a2..ea8d1caa39 100644
--- a/ui-ngx/src/app/core/auth/auth.service.ts
+++ b/ui-ngx/src/app/core/auth/auth.service.ts
@@ -20,6 +20,7 @@ import { HttpClient } from '@angular/common/http';
import { Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
+import { setEdqsEnabled, setNullsOrderStrategy } from '@shared/models/page/page-link';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { Router, UrlTree } from '@angular/router';
@@ -456,6 +457,8 @@ export class AuthService {
return this.http.get('/api/system/params', defaultHttpOptions()).pipe(
map((sysParams) => {
this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit);
+ setNullsOrderStrategy(sysParams.nullsOrderStrategy);
+ setEdqsEnabled(sysParams.edqsEnabled);
return sysParams;
}),
catchError(() => of({} as SysParamsState))
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
index 72f4b0f1d8..500d974726 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-