Browse Source

Merge remote-tracking branch 'upstream/feature/iot-hub' into add-related-items

pull/15682/head
dpinkevych 3 months ago
parent
commit
47dae5cfc1
  1. 2
      application/pom.xml
  2. 2
      application/src/main/data/json/system/widget_types/html_container.json
  3. 12
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  4. 2
      common/actor/pom.xml
  5. 2
      common/cache/pom.xml
  6. 2
      common/cluster-api/pom.xml
  7. 2
      common/coap-server/pom.xml
  8. 2
      common/dao-api/pom.xml
  9. 2
      common/data/pom.xml
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  11. 2
      common/discovery-api/pom.xml
  12. 2
      common/edge-api/pom.xml
  13. 1
      common/edge-api/src/main/proto/edge.proto
  14. 2
      common/edqs/pom.xml
  15. 2
      common/message/pom.xml
  16. 2
      common/pom.xml
  17. 2
      common/proto/pom.xml
  18. 2
      common/queue/pom.xml
  19. 2
      common/script/pom.xml
  20. 2
      common/script/remote-js-client/pom.xml
  21. 2
      common/script/script-api/pom.xml
  22. 2
      common/stats/pom.xml
  23. 2
      common/transport/coap/pom.xml
  24. 2
      common/transport/http/pom.xml
  25. 2
      common/transport/lwm2m/pom.xml
  26. 2
      common/transport/mqtt/pom.xml
  27. 2
      common/transport/pom.xml
  28. 2
      common/transport/snmp/pom.xml
  29. 2
      common/transport/transport-api/pom.xml
  30. 74
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java
  31. 64
      common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java
  32. 2
      common/util/pom.xml
  33. 2
      common/version-control/pom.xml
  34. 2
      dao/pom.xml
  35. 2
      edqs/pom.xml
  36. 2
      monitoring/pom.xml
  37. 2
      msa/black-box-tests/pom.xml
  38. 2
      msa/edqs/pom.xml
  39. 2
      msa/js-executor/package.json
  40. 2
      msa/js-executor/pom.xml
  41. 2
      msa/monitoring/pom.xml
  42. 2
      msa/pom.xml
  43. 2
      msa/tb-node/pom.xml
  44. 2
      msa/tb/pom.xml
  45. 2
      msa/transport/coap/pom.xml
  46. 2
      msa/transport/http/pom.xml
  47. 2
      msa/transport/lwm2m/pom.xml
  48. 2
      msa/transport/mqtt/pom.xml
  49. 2
      msa/transport/pom.xml
  50. 2
      msa/transport/snmp/pom.xml
  51. 2
      msa/vc-executor-docker/pom.xml
  52. 2
      msa/vc-executor/pom.xml
  53. 2
      msa/web-ui/package.json
  54. 2
      msa/web-ui/pom.xml
  55. 4
      netty-mqtt/pom.xml
  56. 4
      pom.xml
  57. 2
      rest-client/pom.xml
  58. 2
      rule-engine/pom.xml
  59. 2
      rule-engine/rule-engine-api/pom.xml
  60. 2
      rule-engine/rule-engine-components/pom.xml
  61. 2
      tools/pom.xml
  62. 2
      transport/coap/pom.xml
  63. 2
      transport/http/pom.xml
  64. 2
      transport/lwm2m/pom.xml
  65. 2
      transport/mqtt/pom.xml
  66. 2
      transport/pom.xml
  67. 2
      transport/snmp/pom.xml
  68. 2
      ui-ngx/package.json
  69. 2
      ui-ngx/pom.xml
  70. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  71. 3
      ui-ngx/src/app/core/auth/auth.models.ts
  72. 2
      ui-ngx/src/app/core/auth/auth.reducer.ts
  73. 3
      ui-ngx/src/app/core/auth/auth.service.ts
  74. 33
      ui-ngx/src/app/core/http/iot-hub-api.service.ts
  75. 22
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  76. 153
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  77. 15
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  78. 18
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts
  79. 10
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
  80. 96
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html
  81. 95
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts
  82. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html
  83. 5
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss
  84. 3
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  85. 5
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss
  86. 40
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html
  87. 77
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss
  88. 47
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts
  89. 177
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html
  90. 42
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss
  91. 48
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts
  92. 43
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html
  93. 76
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss
  94. 59
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts
  95. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
  96. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  97. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html
  98. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts
  99. 16
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  100. 15
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

2
application/src/main/data/json/system/widget_types/html_container.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>",
"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",

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

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -39,5 +39,7 @@ public class SystemParams {
long maxArgumentsPerCF;
long maxDataPointsPerRollingArg;
TrendzSettings trendzSettings;
String nullsOrderStrategy;
boolean edqsEnabled;
String iotHubBaseUrl;
}

2
common/discovery-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

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

@ -48,6 +48,7 @@ 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_LATEST = 99999;
}

2
common/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

74
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<Path> paths;
private final Runnable reloadCallback;
private final Map<Path, Long> lastModifiedMap;
private final Map<Path, String> lastChecksumMap;
private int consecutiveFailures;
private String failedCombinedChecksum;
@ -173,60 +171,23 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
CertificateWatcher(List<Path> 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<Path, Long> currentModifiedTimes = new HashMap<>();
Map<Path, String> 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<Path, String> 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) {

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

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

2
edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>edqs</artifactId>

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "4.2.2.2",
"version": "4.2.2.3",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.msa</groupId>
<artifactId>transport</artifactId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/vc-executor-docker/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/vc-executor/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/web-ui/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
"version": "4.2.2.2",
"version": "4.2.2.3",
"description": "ThingsBoard Web UI Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/web-ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

4
netty-mqtt/pom.xml

@ -19,11 +19,11 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>netty-mqtt</artifactId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Netty MQTT Client</name>

4
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>
@ -69,7 +69,7 @@
<spring-boot-test.version>3.5.13</spring-boot-test.version>
<commons-lang3.version>3.18.0</commons-lang3.version> <!-- to fix CVE-2025-48924. TODO: remove when fixed in spring-boot-dependencies -->
<postgresql.version>42.7.11</postgresql.version> <!-- to fix CVE-2026-42198. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.133.Final</netty.version> <!-- to fix CVE-2026-42579, CVE-2026-42583, CVE-2026-42584, CVE-2026-42587. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.134.Final</netty.version> <!-- to fix CVE-2026-42579, CVE-2026-42583, CVE-2026-42584, CVE-2026-42587, and MQTT decoder regression introduced in 4.1.133 by the CVE-2026-44248 fix. TODO: remove when fixed in spring-boot-dependencies -->
<tomcat.version>10.1.55</tomcat.version> <!-- to fix CVE-2026-41284, CVE-2026-43512. TODO: remove when fixed in spring-boot-dependencies -->
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jjwt.version>0.12.5</jjwt.version>

2
rest-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rest-client</artifactId>

2
rule-engine/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rule-engine</artifactId>

2
rule-engine/rule-engine-api/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

2
rule-engine/rule-engine-components/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>tools</artifactId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>transport</artifactId>

2
transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
ui-ngx/package.json

@ -1,6 +1,6 @@
{
"name": "thingsboard",
"version": "4.2.2.2",
"version": "4.2.2.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",

2
ui-ngx/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.2.2.2-SNAPSHOT</version>
<version>4.2.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -110,6 +110,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<any>;

3
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;
@ -34,6 +35,8 @@ export interface SysParamsState {
ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
trendzSettings: TrendzSettings;
nullsOrderStrategy: NullsOrderStrategy;
edqsEnabled: boolean;
iotHubBaseUrl: string;
}

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

@ -37,6 +37,8 @@ const emptyUserAuthState: AuthPayload = {
maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings,
trendzSettings: initialTrendzSettings,
nullsOrderStrategy: 'default',
edqsEnabled: false,
iotHubBaseUrl: ''
};

3
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';
@ -441,6 +442,8 @@ export class AuthService {
return this.http.get<SysParams>('/api/system/params', defaultHttpOptions()).pipe(
map((sysParams) => {
this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit);
setNullsOrderStrategy(sysParams.nullsOrderStrategy);
setEdqsEnabled(sysParams.edqsEnabled);
return sysParams;
}),
catchError(() => of({} as SysParamsState))

33
ui-ngx/src/app/core/http/iot-hub-api.service.ts

@ -39,6 +39,16 @@ export function tbVersionToInt(version: string): number {
return major * 100 + minor * 10 + patch;
}
// Inverse of tbVersionToInt — produces "{major}.{minor}" by default
// and appends ".{patch}" when the patch component is non-zero so the
// rendered version stays compact (e.g. 430 → "4.3", 421 → "4.2.1").
export function tbVersionIntToString(version: number): string {
const major = Math.floor(version / 100);
const minor = Math.floor((version % 100) / 10);
const patch = version % 10;
return patch === 0 ? `${major}.${minor}` : `${major}.${minor}.${patch}`;
}
export function iotHubResourceUrl(baseUrl: string, path: string): string {
if (!path) {
return path;
@ -121,6 +131,29 @@ export class IotHubApiService {
);
}
/**
* Resolves the listing item-version that matches the caller's edition
* and platform version.
*
* Backend: GET /api/listings/public/by-slug/{slug}/item-version
*
* On success 200 with `MpItemVersionView`.
* On miss 404 with a `ListingItemVersionNotFound` body shape
* (`noMatchingVersions`, `peRequired`, or
* `minTbVersionRequired`). Callers should inspect
* `HttpErrorResponse.error` to differentiate.
*/
public getListingItemVersion(slug: string, config?: IotHubRequestConfig): Observable<MpItemVersionView> {
const queryParams = [
'ce=true',
`tbVersion=${tbVersionToInt(env.tbVersion)}`
];
return this.http.get<MpItemVersionView>(
`${this.baseUrl}/api/listings/public/by-slug/${encodeURIComponent(slug)}/item-version?${queryParams.join('&')}`,
{ params: this.buildParams(config) }
);
}
public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable<string> {
return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, {
params: this.buildParams(config),

22
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -26,7 +26,7 @@ import {
widgetType,
WidgetTypeInfo
} from '@shared/models/widget.models';
import { debounceTime, distinctUntilChanged, map, skip, switchMap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, forkJoin, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
@ -780,21 +780,17 @@ export class DashboardWidgetSelectComponent {
}
private fetchInstalledWidgetVersions() {
const itemIds = (this.installedWidgets || []).map(i => i.itemId);
if (itemIds.length === 0) {
const versionIds = (this.installedWidgets || [])
.map(i => i.itemVersionId)
.filter(id => !!id);
if (versionIds.length === 0) {
this.installedWidgetVersions = [];
return of([]);
}
return this.iotHubApiService.getItemsPublishedVersions(itemIds, { ignoreLoading: true }).pipe(
switchMap(infos => {
if (infos.length === 0) {
return of([]);
}
const versionRequests = infos.map(info =>
this.iotHubApiService.getVersionInfo(info.publishedVersionId, { ignoreLoading: true })
);
return forkJoin(versionRequests);
}),
const versionRequests = versionIds.map(id =>
this.iotHubApiService.getVersionInfo(id, { ignoreLoading: true })
);
return forkJoin(versionRequests).pipe(
map(versions => {
this.installedWidgetVersions = versions.sort((a, b) => b.totalInstallCount - a.totalInstallCount);
return this.installedWidgetVersions;

153
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html

@ -405,87 +405,92 @@
</button>
</div>
<!-- Loading -->
@if (isLoading) {
<div class="flex justify-center py-20">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
<div class="relative">
<!-- Error -->
@if (hasError && !isLoading) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.unable-to-load' | translate }}</h3>
<p>{{ 'iot-hub.unable-to-load-text' | translate }}</p>
<button mat-flat-button color="primary" (click)="loadItems()">
{{ 'iot-hub.retry' | translate }}
</button>
</div>
}
<!-- Empty state -->
@if (!isLoading && !hasError && items.length === 0) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.no-items-found-text' | translate }}</p>
@if (hasActiveFilters()) {
<button mat-flat-button color="primary" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all-filters' | translate }}
</button>
<!-- Error -->
@if (hasError) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-service-bg"></div>
<h3>{{ 'iot-hub.network-server-unavailable' | translate }}</h3>
<p>{{ 'iot-hub.network-server-unavailable-text' | translate }}</p>
<button mat-flat-button color="primary" (click)="retryLoadItems()">
{{ 'iot-hub.try-again' | translate }}
</button>
</div>
}
</div>
}
<!-- Card grid -->
@if (!isLoading && !hasError && items.length > 0) {
<div class="tb-iot-hub-card-grid" [class.tb-iot-hub-card-grid-compact]="isCompactType">
@for (item of items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="!fixedSubType"
[showCreator]="!creatorId"
[mode]="mode"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)"
(addClick)="onItemAdd($event)"
(guideClick)="openInstallGuide($event)">
</tb-iot-hub-item-card>
<!-- Empty state -->
@if (!hasError && items.length === 0) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.no-items-found-text' | translate }}</p>
@if (hasActiveFilters()) {
<button mat-flat-button color="primary" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all-filters' | translate }}
</button>
}
</div>
}
</div>
<!-- Pagination -->
<div class="tb-iot-hub-pagination flex shrink-0 items-center pt-6">
<div class="tb-iot-hub-pagination-pages flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-iot-hub-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= getTotalPages() - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-iot-hub-pagination-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-iot-hub-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
<!-- Card grid -->
@if (!hasError && items.length > 0) {
<div class="tb-iot-hub-card-grid" [class.tb-iot-hub-card-grid-compact]="isCompactType">
@for (item of items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="!fixedSubType"
[showCreator]="!creatorId"
[mode]="mode"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)"
(addClick)="onItemAdd($event)"
(guideClick)="openInstallGuide($event)">
</tb-iot-hub-item-card>
}
</mat-select>
</div>
</div>
<!-- Pagination -->
<div class="tb-iot-hub-pagination flex shrink-0 items-center pt-6">
<div class="tb-iot-hub-pagination-pages flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-iot-hub-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= getTotalPages() - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-iot-hub-pagination-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-iot-hub-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
</mat-select>
</div>
</div>
}
<!-- Loading -->
@if (isLoading) {
<div class="absolute inset-0 backdrop-grayscale" style="background: rgba(255, 255, 255, 0.72)">
<div class="flex justify-center py-20">
<mat-spinner diameter="64"></mat-spinner>
</div>
</div>
}
</div>
}
</div>
</mat-drawer-content>
</mat-drawer-container>

15
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss

@ -112,7 +112,8 @@
}
.tb-iot-hub-filters-wrapper {
position: absolute;
position: sticky;
max-height: calc(100vh - var(--mat-toolbar-standard-height, 64px));
inset: 0;
overflow-y: auto;
}
@ -146,10 +147,12 @@
width: 60%;
max-width: 550px;
min-width: 300px;
margin-bottom: -48px;
.embedded & {
margin-bottom: 0;
&:not(.embedded &) {
position: fixed;
top: var(--mat-toolbar-standard-height, 64px);
@media #{$mat-xs} {
top: var(--mat-toolbar-mobile-height, 56px);
}
}
}
@ -421,7 +424,7 @@
// Empty state text + no-data image
.tb-iot-hub-empty-state {
.tb-no-data-bg {
.tb-no-data-bg, .tb-no-service-bg {
flex: auto;
height: 100px;
}

18
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.ts

@ -113,6 +113,7 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
isLoading = false;
hasError = false;
filterDrawerOpened = false;
private retryTimer: any = null;
textSearch = '';
_activeType: ItemType = ItemType.WIDGET;
@ -676,9 +677,23 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
});
}
retryLoadItems(): void {
if (this.retryTimer != null) {
clearTimeout(this.retryTimer);
}
this.isLoading = true;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
this.loadItems();
}, 350);
}
loadItems(): void {
if (this.retryTimer != null) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.isLoading = true;
this.hasError = false;
const sort = this.sortOptions[this.selectedSortIndex];
const sortOrder: SortOrder = { property: sort.value, direction: sort.direction };
const pageLink = new PageLink(this.pageSize, this.pageIndex, this.textSearch || null, sortOrder);
@ -702,6 +717,7 @@ export class TbIotHubBrowseComponent implements OnInit, AfterViewInit, OnDestroy
this.items = data.data;
this.totalElements = data.totalElements;
this.isLoading = false;
this.hasError = false;
},
error: () => {
this.isLoading = false;

10
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts

@ -31,6 +31,8 @@ import { TbIotHubSearchComponent } from './iot-hub-search.component';
import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-table.component';
import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component';
import { TbPeConnectivityMethodPromptComponent } from './pe-connectivity-method-prompt.component';
import { TbIotHubPeRequiredDialogComponent } from './iot-hub-pe-required-dialog.component';
import { TbIotHubUpgradeRequiredDialogComponent } from './iot-hub-upgrade-required-dialog.component';
import { TbIotHubMarkdownComponent } from './iot-hub-markdown.component';
import { SolutionInstallDialogComponent } from './solution-install-dialog.component';
import { IotHubActionsService } from './iot-hub-actions.service';
@ -53,7 +55,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent,
InstallFormRendererComponent
InstallFormRendererComponent,
TbIotHubPeRequiredDialogComponent,
TbIotHubUpgradeRequiredDialogComponent
],
imports: [
CommonModule,
@ -78,7 +82,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbIotHubInstalledItemsDialogComponent,
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent
SolutionInstallDialogComponent,
TbIotHubPeRequiredDialogComponent,
TbIotHubUpgradeRequiredDialogComponent
]
})
export class IotHubComponentsModule { }

96
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html

@ -19,26 +19,55 @@
@switch (state) {
@case ('confirm') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
@if (item.type === ItemType.RULE_CHAIN) {
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.rule-chain-install-desc' | translate }}</p>
} @else {
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
}
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-desc' | translate }}</p>
}
@case ('select-entity') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}</h2>
@if (activeSelectEntityConfig?.promptKey) {
<p class="tb-iot-hub-install-desc"
[innerHTML]="activeSelectEntityConfig.promptKey | translate:{ name: item.name }"></p>
@if (item.type === ItemType.RULE_CHAIN) {
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.rule-chain-install-subtitle' | translate }}</p>
<form [formGroup]="ruleChainInstallForm">
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="setAsDefault">
<div tb-hint-tooltip-icon="{{ 'iot-hub.rule-chain-set-as-profile-default-tooltip' | translate }}">
{{ 'iot-hub.rule-chain-set-as-profile-default-toggle' | translate }}
</div>
</mat-slide-toggle>
@if (ruleChainInstallForm.controls.setAsDefault.value) {
<div class="flex flex-row gap-4 xs:flex-col xs:gap-0">
<tb-entity-type-select
class="flex-1"
appearance="outline"
[showLabel]="true"
[required]="true"
[allowedEntityTypes]="[EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]"
[filterAllowedEntityTypes]="false"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-autocomplete
class="flex-1"
appearance="outline"
[required]="true"
[entityType]="ruleChainInstallForm.controls.entityType.value"
formControlName="entityId">
</tb-entity-autocomplete>
</div>
}
</div>
</form>
} @else {
@if (activeSelectEntityConfig?.promptKey) {
<p class="tb-iot-hub-install-desc"
[innerHTML]="activeSelectEntityConfig.promptKey | translate:{ name: item.name }"></p>
}
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="activeSelectEntityConfig.allowed"
[defaultEntityType]="activeSelectEntityConfig.defaultType"
[filterAllowedEntityTypes]="false"
appearance="outline"
[required]="activeSelectEntityConfig.required">
</tb-entity-select>
}
<tb-entity-select
[(ngModel)]="selectedEntityId"
[allowedEntityTypes]="activeSelectEntityConfig.allowed"
[defaultEntityType]="activeSelectEntityConfig.defaultType"
[filterAllowedEntityTypes]="false"
appearance="outline"
[required]="activeSelectEntityConfig.required">
</tb-entity-select>
}
@case ('confirm-overwrite') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.rule-chain-overwrite-title' | translate }}</h2>
@ -112,7 +141,7 @@
@case ('error') {
<h2 class="tb-iot-hub-install-title">{{ 'iot-hub.install-error-title' | translate }}</h2>
<p class="tb-iot-hub-install-desc">{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}</p>
@if (item?.type === ItemType.SOLUTION_TEMPLATE) {
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<tb-iot-hub-markdown class="tb-iot-hub-error-details-markdown" [data]="errorMessage"></tb-iot-hub-markdown>
} @else {
<div class="tb-iot-hub-error-details">
@ -126,32 +155,19 @@
@switch (state) {
@case ('confirm') {
<button mat-button [disabled]="resolvingPlan" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
@if (item.type === ItemType.RULE_CHAIN) {
<button mat-stroked-button [disabled]="resolvingPlan" (click)="install()">
@if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner>
}
{{ 'iot-hub.install' | translate }}
</button>
<button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="installAsEntityProfileDefault()">
{{ 'iot-hub.rule-chain-install-as-default' | translate }}
</button>
} @else {
<button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="install()">
@if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner>
}
{{ 'iot-hub.install' | translate }}
</button>
}
<button mat-flat-button color="primary" [disabled]="resolvingPlan" (click)="install()">
@if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner>
}
{{ 'iot-hub.install' | translate }}
</button>
}
@case ('select-entity') {
<button mat-button [disabled]="resolvingPlan" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
@if (item.type === ItemType.RULE_CHAIN) {
<button mat-button [disabled]="resolvingPlan" (click)="selectEntityBack()">{{ 'action.back' | translate }}</button>
<button mat-flat-button color="primary"
[disabled]="!selectedEntityId || resolvingPlan"
(click)="onSelectEntityInstall()">
[disabled]="ruleChainInstallForm.invalid || resolvingPlan"
(click)="onRuleChainInstall()">
@if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner>
}
@ -159,8 +175,8 @@
</button>
} @else {
<button mat-flat-button color="primary"
[disabled]="(activeSelectEntityConfig?.required && !selectedEntityId) || resolvingPlan"
(click)="onSelectEntityInstall()">
[disabled]="(activeSelectEntityConfig.required && !selectedEntityId) || resolvingPlan"
(click)="onEntitySelectInstall()">
@if (resolvingPlan) {
<mat-spinner diameter="18" class="mr-2 inline-block align-middle"></mat-spinner>
}

95
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts

@ -14,9 +14,11 @@
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { Component, DestroyRef, Inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
@ -80,15 +82,21 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
ItemType = ItemType;
PlanStatus = InstallPlanEntryStatus;
EntityType = EntityType;
item: MpItemVersionView;
typeTranslations = itemTypeTranslations;
state: InstallState = 'confirm';
state!: InstallState;
errorMessage = '';
entityDetailsUrl: string | null = null;
selectedEntityId: EntityId | null = null;
pendingOverwrite: PendingOverwrite | null = null;
ruleChainInstallForm!: FormGroup<{
setAsDefault: FormControl<boolean>;
entityType: FormControl<EntityType>;
entityId: FormControl<string | null>;
}>;
installPlan: InstallPlan | null = null;
planSummary: { willInstall: number; alreadyInstalled: number; missing: number } = {
@ -106,12 +114,6 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
required: true,
promptKey: 'iot-hub.select-entity-for-cf',
},
[ItemType.RULE_CHAIN]: {
allowed: [EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE],
defaultType: EntityType.DEVICE_PROFILE,
required: true,
promptKey: 'iot-hub.select-profile-for-rule-chain',
},
};
get activeSelectEntityConfig(): SelectEntityConfig | null {
@ -128,10 +130,47 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
private iotHubApiService: IotHubApiService,
private deviceProfileService: DeviceProfileService,
private assetProfileService: AssetProfileService,
private ruleChainService: RuleChainService
private ruleChainService: RuleChainService,
private fb: FormBuilder,
private destroyRef: DestroyRef
) {
super(store, router, dialogRef);
this.item = data.item;
this.state = this.computeInitialState();
if (this.item.type === ItemType.RULE_CHAIN) {
this.initRuleChainForm();
}
}
private computeInitialState(): InstallState {
return this.activeSelectEntityConfig || this.item.type === ItemType.RULE_CHAIN
? 'select-entity'
: 'confirm';
}
private initRuleChainForm(): void {
this.ruleChainInstallForm = this.fb.group({
setAsDefault: this.fb.nonNullable.control<boolean>(false),
entityType: this.fb.nonNullable.control<EntityType>(EntityType.DEVICE_PROFILE),
entityId: this.fb.control<string | null>(null)
});
this.ruleChainInstallForm.controls.setAsDefault.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(setAsDefault => {
const entityIdCtrl = this.ruleChainInstallForm.controls.entityId;
if (setAsDefault) {
entityIdCtrl.setValidators(Validators.required);
} else {
entityIdCtrl.clearValidators();
entityIdCtrl.setValue(null);
}
entityIdCtrl.updateValueAndValidity();
});
this.ruleChainInstallForm.controls.entityType.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.ruleChainInstallForm.controls.entityId.setValue(null);
});
}
getTypeLabel(): string {
@ -139,39 +178,33 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
return key ? this.translate.instant(key) : '';
}
install(): void {
if (this.item.type === ItemType.CALCULATED_FIELD) {
this.state = 'select-entity';
onEntitySelectInstall(): void {
if (!this.selectedEntityId) {
return;
}
this.doInstall();
}
installAsEntityProfileDefault(): void {
this.state = 'select-entity';
}
selectEntityBack(): void {
this.selectedEntityId = null;
this.state = 'confirm';
this.install();
}
onSelectEntityInstall(): void {
if (!this.selectedEntityId) {
onRuleChainInstall(): void {
const { setAsDefault, entityType, entityId } = this.ruleChainInstallForm.getRawValue();
if (!setAsDefault) {
this.selectedEntityId = null;
this.install();
return;
}
if (this.item.type !== ItemType.RULE_CHAIN) {
this.doInstall();
if (!entityId) {
return;
}
this.resolveOverwrite(this.selectedEntityId).subscribe({
const profileEntityId: EntityId = { entityType, id: entityId };
this.selectedEntityId = profileEntityId;
this.resolveOverwrite(profileEntityId).subscribe({
next: (pending) => {
if (pending) {
this.pendingOverwrite = pending;
this.state = 'confirm-overwrite';
} else {
this.pendingOverwrite = null;
this.doInstall();
this.install();
}
},
error: (err) => this.handleApiError(err)
@ -179,7 +212,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
}
confirmOverwriteReplace(): void {
this.doInstall();
this.install();
}
confirmOverwriteCancel(): void {
@ -213,7 +246,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
);
}
doInstall(): void {
install(): void {
// Keep the current state visible (confirm / select-entity / confirm-overwrite) while resolving —
// a transient 'resolving-plan' state caused a visible "blink" for items without dependencies.
this.resolvingPlan = true;
@ -335,7 +368,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent<TbIotHubInst
cancelPlan(): void {
this.installPlan = null;
this.state = 'confirm';
this.state = this.computeInitialState();
}
openEntityDetails(): void {

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html

@ -80,7 +80,7 @@
} @else {
<!-- Big card (Widget / Dashboard / Solution Template) -->
<div class="tb-iot-hub-item-card" role="button" tabindex="0" (click)="onCardClick()" (keyup.enter)="onCardClick()">
<div class="tb-iot-hub-card-preview" [class.tb-iot-hub-card-preview-device]="item.type === ItemType.DEVICE">
<div class="tb-iot-hub-card-preview">
@if (getPreviewUrl(); as previewUrl) {
<img [src]="previewUrl" alt="" class="tb-iot-hub-preview-image">
} @else {

5
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss

@ -52,11 +52,6 @@
position: relative;
overflow: hidden;
padding: 12px;
&.tb-iot-hub-card-preview-device {
background: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}
.tb-iot-hub-preview-image {

3
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html

@ -94,8 +94,7 @@
<!-- Widget/Dashboard/Solution: image + description side by side -->
<div class="flex gap-6">
<div class="dlg-preview"
[class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1"
[class.dlg-preview-device]="item.type === ItemType.DEVICE">
[class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<div class="dlg-hero-carousel">
<ngx-hm-carousel

5
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -143,11 +143,6 @@
&.dlg-preview-carousel {
aspect-ratio: 1.5;
}
&.dlg-preview-device {
background: white;
border: 1px solid rgba(0, 0, 0, 0.12);
}
}
.dlg-preview-img {

40
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html

@ -0,0 +1,40 @@
<!--
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.
-->
<div class="tb-iot-hub-pe-required">
<button mat-icon-button class="tb-iot-hub-pe-required-close" (click)="close()" tabindex="-1">
<mat-icon>close</mat-icon>
</button>
<span class="tb-iot-hub-pe-required-icon" aria-hidden="true"></span>
<h2 class="tb-iot-hub-pe-required-title">
{{ 'iot-hub.pe-required-title' | translate }}
</h2>
<p class="tb-iot-hub-pe-required-message"
[innerHTML]="'iot-hub.pe-required-message' | translate"></p>
<div class="tb-iot-hub-pe-required-actions">
<button mat-button (click)="close()">
{{ 'action.close' | translate }}
</button>
<button mat-flat-button color="primary" (click)="upgradeInstance()">
{{ 'iot-hub.alarm-rules-unavailable-upgrade' | translate }}
</button>
</div>
</div>

77
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss

@ -0,0 +1,77 @@
/**
* 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.
*/
@import "../../../../../scss/constants";
:host {
display: block;
width: 500px;
max-width: 100%;
}
.tb-iot-hub-pe-required {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
padding: 40px;
background: white;
border-radius: 8px;
}
.tb-iot-hub-pe-required-close {
position: absolute;
top: 8px;
right: 8px;
}
// SVG illustration applied as a mask so the silhouette is filled
// with $tb-primary-color. PE builds inherit their own primary value
// without re-exporting the asset.
.tb-iot-hub-pe-required-icon {
display: block;
width: 100px;
height: 100px;
background-color: $tb-primary-color;
-webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
}
.tb-iot-hub-pe-required-title {
font-size: 24px;
font-weight: 500;
line-height: 32px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
.tb-iot-hub-pe-required-message {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
.tb-iot-hub-pe-required-actions {
display: flex;
align-items: center;
gap: 8px;
}

47
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts

@ -0,0 +1,47 @@
///
/// 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.
///
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
@Component({
selector: 'tb-iot-hub-pe-required-dialog',
standalone: false,
templateUrl: './iot-hub-pe-required-dialog.component.html',
styleUrls: ['./iot-hub-pe-required-dialog.component.scss']
})
export class TbIotHubPeRequiredDialogComponent extends DialogComponent<TbIotHubPeRequiredDialogComponent> {
constructor(
protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubPeRequiredDialogComponent>
) {
super(store, router, dialogRef);
}
close(): void {
this.dialogRef.close();
}
upgradeInstance(): void {
window.open('https://thingsboard.io/docs/pe/installation/upgrade-from-ce/', '_blank');
}
}

177
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html

@ -15,13 +15,11 @@
limitations under the License.
-->
<div class="flex items-baseline gap-4 lt-md:-mb-6">
<div class="mb-10 flex items-baseline gap-4 lt-md:mb-4">
<ng-content></ng-content>
@if (!isLoading && totalElements > 0) {
<span class="tb-search-results-count tb-search-results-count-mobile">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
}
<span class="tb-search-results-count tb-search-results-count-mobile">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
</div>
<!-- Search + Sort row -->
<div class="flex items-center gap-4">
@ -56,92 +54,105 @@
</div>
<!-- Results count + Pagination (shown when results exist) -->
@if (!isLoading && totalElements > 0) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
<div class="tb-search-pagination-row" [class.lt-md:!hidden]="totalElements === 0">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
@if (totalElements > 0) {
<div class="tb-search-pagination">
<div class="flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
<div class="flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-search-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-search-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= totalPages - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-search-page-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-search-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
<button mat-icon-button [disabled]="pageIndex >= totalPages - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-search-page-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-search-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
</mat-select>
</div>
</mat-select>
</div>
</div>
}
}
</div>
<!-- No results count when 0 -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: 0 } }}
</span>
</div>
}
<div class="tb-search-divider"></div>
<!-- Loading -->
@if (isLoading) {
<div class="tb-search-loading flex justify-center py-12">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
<div class="relative min-h-0">
<!-- No results -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-empty flex flex-col items-center py-10">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.try-adjusting-search' | translate }}</p>
</div>
}
<!-- Results grouped by type -->
@if (!isLoading && resultGroups.length > 0) {
<div class="tb-search-divider"></div>
<div class="tb-search-categories">
@for (group of resultGroups; track group.type) {
<div class="tb-search-section">
<!-- Section header -->
<button mat-button class="tb-search-section-header" (click)="navigateToType(group.type)">
{{ getTypeLabel(group.type) }}
<mat-icon>chevron_right</mat-icon>
<div class="tb-results-container">
<!-- Error -->
@if (hasError) {
<div class="tb-search-empty flex flex-col items-center py-10">
<div class="tb-no-service-bg"></div>
<h3>{{ 'iot-hub.network-server-unavailable' | translate }}</h3>
<p>{{ 'iot-hub.network-server-unavailable-text' | translate }}</p>
<button mat-flat-button color="primary" (click)="retryLoadResults()">
{{ 'iot-hub.try-again' | translate }}
</button>
<!-- Card grid -->
<div class="tb-search-card-grid" [class.tb-search-card-grid-compact]="isCompactType(group.type)">
@for (item of group.items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="false"
[showCreator]="showCreator"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
<!-- No results -->
@if (!hasError && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-empty flex flex-col items-center py-10">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.try-adjusting-search' | translate }}</p>
</div>
}
<!-- Results grouped by type -->
@if (resultGroups.length > 0) {
<div class="tb-search-categories">
@for (group of resultGroups; track group.type) {
<div class="tb-search-section">
<!-- Section header -->
<button mat-button class="tb-search-section-header" (click)="navigateToType(group.type)">
{{ getTypeLabel(group.type) }}
<mat-icon>chevron_right</mat-icon>
</button>
<!-- Card grid -->
<div class="tb-search-card-grid" [class.tb-search-card-grid-compact]="isCompactType(group.type)">
@for (item of group.items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="false"
[showCreator]="showCreator"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
</div>
}
</div>
}
<!-- Loading -->
@if (isLoading) {
<div class="absolute inset-0 z-10 backdrop-grayscale" style="background: rgba(255, 255, 255, 0.72)">
<div class="flex justify-center py-32">
<mat-spinner diameter="64"></mat-spinner>
</div>
</div>
}
</div>

42
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss

@ -20,7 +20,6 @@
:host {
display: flex;
flex-direction: column;
gap: 40px;
}
.tb-search-results-count-mobile {
@ -54,10 +53,11 @@
// Results count + Pagination row
.tb-search-pagination-row {
flex: 0 0 48px;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: -28px;
margin-top: 12px;
}
.tb-search-results-count {
@ -121,35 +121,24 @@
}
}
// Loading / Empty negative margin
.tb-search-loading,
.tb-search-empty {
margin-top: -20px;
}
// Empty state no-data-bg + text
.tb-search-empty {
.tb-no-data-bg {
.tb-no-data-bg, .tb-no-service-bg {
flex: auto;
height: 100px;
}
h3 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0.1px;
color: rgba(0, 0, 0, 0.76);
margin: 0 0 8px;
font-size: 18px;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
margin: 16px 0 8px;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
margin: 0;
margin: 0 0 24px;
}
}
@ -157,8 +146,15 @@
.tb-search-divider {
background: rgba(0, 0, 0, 0.12);
height: 1px;
margin-bottom: -20px;
margin-top: -20px;
min-height: 1px;
margin-top: 20px;
}
.tb-results-container {
position: relative;
height: 100%;
padding-top: 20px;
overflow: auto;
}
// Categories / sections
@ -166,9 +162,6 @@
display: flex;
flex-direction: column;
gap: 48px;
margin-top: -20px;
overflow: auto;
padding-top: 20px;
}
// Section header
@ -234,6 +227,7 @@
.tb-search-pagination-row {
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 12px;
}

48
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.ts

@ -18,7 +18,7 @@ import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angu
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
@ -65,6 +65,8 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
resultGroups: SearchResultGroup[] = [];
totalElements = 0;
isLoading = false;
hasError = false;
private retryTimer: any = null;
pageSize = 15;
pageIndex = 0;
@ -98,14 +100,10 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
this.loadInstalledItems();
this.searchSubscription = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(text => {
this.isLoading = true;
this.pageIndex = 0;
return this.fetchResults(text);
})
).subscribe(result => {
this.applyResults(result.data, result.totalElements);
distinctUntilChanged()
).subscribe(() => {
this.pageIndex = 0;
this.loadResults();
});
this.loadResults();
}
@ -269,11 +267,37 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
void this.router.navigate(['/iot-hub/creator', creatorId]);
}
retryLoadResults(): void {
if (this.retryTimer != null) {
clearTimeout(this.retryTimer);
}
this.isLoading = true;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
this.loadResults();
}, 350);
}
// Data loading
private loadResults(): void {
if (this.retryTimer != null) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.isLoading = true;
this.fetchResults(this.searchText || '').subscribe(result => {
this.applyResults(result.data, result.totalElements);
// hasError stays as-is until the request actually succeeds
// (cleared in the `next` callback below).
this.fetchResults(this.searchText || '').subscribe({
next: result => {
this.applyResults(result.data, result.totalElements);
this.hasError = false;
},
error: () => {
this.isLoading = false;
this.hasError = true;
this.resultGroups = [];
this.totalElements = 0;
}
});
}
@ -282,7 +306,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
const sortOrder: SortOrder = { property: sort.value, direction: sort.direction };
const pageLink = new PageLink(this.pageSize, this.pageIndex, text.trim() || null, sortOrder);
const query = new MpItemVersionQuery(pageLink, { creatorId: this.creatorId || undefined });
return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true });
return this.iotHubApiService.getPublishedVersions(query, { ignoreLoading: true, ignoreErrors: true });
}
private applyResults(data: MpItemVersionView[], totalElements: number): void {

43
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html

@ -0,0 +1,43 @@
<!--
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.
-->
<div class="tb-iot-hub-upgrade-required">
<button mat-icon-button class="tb-iot-hub-upgrade-required-close" (click)="close()" tabindex="-1">
<mat-icon>close</mat-icon>
</button>
<span class="tb-iot-hub-upgrade-required-icon" aria-hidden="true"></span>
<h2 class="tb-iot-hub-upgrade-required-title">
{{ 'iot-hub.upgrade-required-title' | translate }}
</h2>
<p class="tb-iot-hub-upgrade-required-message"
[innerHTML]="'iot-hub.upgrade-required-message' | translate:{
minVersion: minVersion,
currentVersion: currentVersion
}"></p>
<div class="tb-iot-hub-upgrade-required-actions">
<button mat-button (click)="close()">
{{ 'action.close' | translate }}
</button>
<button mat-flat-button color="primary" (click)="upgradeInstance()">
{{ 'iot-hub.alarm-rules-unavailable-upgrade' | translate }}
</button>
</div>
</div>

76
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss

@ -0,0 +1,76 @@
/**
* 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.
*/
@import "../../../../../scss/constants";
:host {
display: block;
width: 500px;
max-width: 100%;
}
.tb-iot-hub-upgrade-required {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
padding: 40px;
background: white;
border-radius: 8px;
}
.tb-iot-hub-upgrade-required-close {
position: absolute;
top: 8px;
right: 8px;
}
// Re-use the upgrade-required.svg silhouette as a CSS mask so the icon
// tints with $tb-primary-color (PE overrides apply automatically).
.tb-iot-hub-upgrade-required-icon {
display: block;
width: 100px;
height: 100px;
background-color: $tb-primary-color;
-webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain;
}
.tb-iot-hub-upgrade-required-title {
font-size: 24px;
font-weight: 500;
line-height: 32px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
.tb-iot-hub-upgrade-required-message {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
.tb-iot-hub-upgrade-required-actions {
display: flex;
align-items: center;
gap: 8px;
}

59
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts

@ -0,0 +1,59 @@
///
/// 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.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { tbVersionIntToString } from '@core/http/iot-hub-api.service';
import { environment as env } from '@env/environment';
export interface IotHubUpgradeRequiredDialogData {
minTbVersion: number;
}
@Component({
selector: 'tb-iot-hub-upgrade-required-dialog',
standalone: false,
templateUrl: './iot-hub-upgrade-required-dialog.component.html',
styleUrls: ['./iot-hub-upgrade-required-dialog.component.scss']
})
export class TbIotHubUpgradeRequiredDialogComponent extends DialogComponent<TbIotHubUpgradeRequiredDialogComponent> {
readonly minVersion: string;
readonly currentVersion: string;
constructor(
protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<TbIotHubUpgradeRequiredDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: IotHubUpgradeRequiredDialogData
) {
super(store, router, dialogRef);
this.minVersion = `v${tbVersionIntToString(data.minTbVersion)}`;
this.currentVersion = `v${env.tbVersion}`;
}
close(): void {
this.dialogRef.close();
}
upgradeInstance(): void {
window.open('https://thingsboard.io/docs/installation/upgrade-instructions/', '_blank');
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-form-panel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div class="tb-form-panel" [class.stroked]="strokedPanel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div class="flex flex-row items-center justify-start gap-4">
<div class="tb-form-panel-title" translate style="padding-right: 48px;">widget-config.actions</div>
<mat-chip-listbox class="flex-1">

5
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts

@ -26,6 +26,7 @@ import {
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-widget-actions-panel',
@ -45,6 +46,10 @@ export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
strokedPanel = false;
actionsFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};

4
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html

@ -17,4 +17,8 @@
-->
<ng-container [formGroup]="htmlContainerWidgetConfigForm">
<tb-html-container-settings formControlName="settings"></tb-html-container-settings>
<tb-widget-actions-panel
formControlName="actions"
strokedPanel>
</tb-widget-actions-panel>
</ng-container>

4
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts

@ -51,12 +51,14 @@ export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponen
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: HtmlContainerWidgetSettings = {...htmlContainerDefaultSettings, ...(configData.config.settings || {})};
this.htmlContainerWidgetConfigForm = this.fb.group({
settings: [settings, []]
settings: [settings, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings};
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
}

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

@ -47,7 +47,7 @@ import {
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link';
import { SortColumnType, sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs';
@ -118,6 +118,7 @@ import {
dataKeyToEntityKey,
dataKeyTypeToEntityKeyType,
entityDataPageLinkSortDirection,
EntityKeyType,
KeyFilter
} from '@app/shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -730,8 +731,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.pageLink.sortOrder = null;
}
const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
const sortColumnType: SortColumnType = key
? (key.type === EntityKeyType.ENTITY_FIELD || key.type === EntityKeyType.ALARM_FIELD ? 'entityField'
: key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute')
: 'entityField';
const keyFilters: KeyFilter[] = null; // TODO:
this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters);
this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, sortColumnType, keyFilters);
this.ctx.detectChanges();
}
@ -1256,6 +1261,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private appliedPageLink: AlarmDataPageLink;
private appliedSortOrderLabel: string;
private appliedSortColumnType: SortColumnType = 'entityField';
private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[];
@ -1294,11 +1300,13 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
this.pageDataSubject.complete();
}
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string,
sortColumnType: SortColumnType, keyFilters: KeyFilter[]) {
this.dataLoading = true;
// this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.appliedSortColumnType = sortColumnType;
this.subscription.subscribeForAlarms(pageLink, keyFilters);
}
@ -1330,7 +1338,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
}
if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) {
const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC;
alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc));
alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType));
}
if (this.selection.hasValue()) {
const alarmIds = alarms.map((alarm) => alarm.id.id);

15
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -100,7 +100,7 @@ import {
EntityKeyType,
KeyFilter
} from '@shared/models/query/query.models';
import { sortItems } from '@shared/models/page/page-link';
import { SortColumnType, sortItems } from '@shared/models/page/page-link';
import { entityFields } from '@shared/models/entity.models';
import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -617,8 +617,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.pageLink.sortOrder = null;
}
const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
const sortColumnType: SortColumnType = key
? (key.type === EntityKeyType.ENTITY_FIELD ? 'entityField'
: key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute')
: 'entityField';
const keyFilters: KeyFilter[] = null; // TODO:
this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters);
this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, sortColumnType, keyFilters);
this.ctx.detectChanges();
}
@ -865,6 +869,7 @@ class EntityDatasource implements DataSource<EntityData> {
private appliedPageLink: EntityDataPageLink;
private appliedSortOrderLabel: string;
private appliedSortColumnType: SortColumnType = 'entityField';
private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[];
@ -905,11 +910,13 @@ class EntityDatasource implements DataSource<EntityData> {
this.pageDataSubject.complete();
}
loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string,
sortColumnType: SortColumnType, keyFilters: KeyFilter[]) {
this.dataLoading = true;
// this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.appliedSortColumnType = sortColumnType;
this.subscription.subscribeForPaginatedData(0, pageLink, keyFilters);
}
@ -934,7 +941,7 @@ class EntityDatasource implements DataSource<EntityData> {
});
if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) {
const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC;
entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc));
entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType));
}
if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) {
maxCellButtonAction = entities[0].actionCellButtons.length;

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

Loading…
Cancel
Save