diff --git a/application/pom.xml b/application/pom.xml index 8e5e6b2564..afb619574a 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard application diff --git a/application/src/main/data/json/system/widget_types/html_container.json b/application/src/main/data/json/system/widget_types/html_container.json index 25b9e5ac5d..a27996593b 100644 --- a/application/src/main/data/json/system/widget_types/html_container.json +++ b/application/src/main/data/json/system/widget_types/html_container.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n", + "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'javaScript': {\n name: 'JavaScript',\n multiple: true\n }\n };\n}", "settingsDirective": "tb-html-container-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-html-container-basic-config", diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 40e88a4c6e..285a250c08 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.dao.trendz.TrendzSettingsService; @@ -52,6 +53,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Hidden @@ -76,9 +78,14 @@ public class SystemInfoController extends BaseController { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + @Value("${sql.entity_data_query_nulls_order_strategy:default}") + private String nullsOrderStrategy; + @Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}") private String iotHubBaseUrl; + private static final Set ACCEPTED_NULLS_ORDER_STRATEGIES = Set.of("default", "nulls_first", "nulls_last"); + @Autowired(required = false) private BuildProperties buildProperties; @@ -94,6 +101,9 @@ public class SystemInfoController extends BaseController { @Autowired private TrendzSettingsService trendzSettingsService; + @Autowired + private EdqsService edqsService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -153,6 +163,8 @@ public class SystemInfoController extends BaseController { } systemParams.setUserSettings(userSettingsNode); systemParams.setMaxDatapointsLimit(maxDatapointsLimit); + systemParams.setNullsOrderStrategy(ACCEPTED_NULLS_ORDER_STRATEGIES.contains(nullsOrderStrategy) ? nullsOrderStrategy : "default"); + systemParams.setEdqsEnabled(edqsService.isApiEnabled()); if (!currentUser.isSystemAdmin()) { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 4d5fff8157..61ff41ce80 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index f3f4f47bb9..a9d43c9ec1 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index bd0c1e6016..987e6e2ffe 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 55f2ee4896..c1a83ee870 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index f2a1fabfbd..c9cfeae4e7 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index e14e7072ea..a9ca14a2d6 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 1da7cfef68..e22e1809a9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -39,5 +39,7 @@ public class SystemParams { long maxArgumentsPerCF; long maxDataPointsPerRollingArg; TrendzSettings trendzSettings; + String nullsOrderStrategy; + boolean edqsEnabled; String iotHubBaseUrl; } diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 0d75e57b9a..1c3b34f6f4 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index bb0d5cb192..ff630f416c 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 70f90cc45b..520b4ac493 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/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; } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 4b2cd36592..f107721524 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 69c4e72e2a..d9184ac548 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index ab2388eb9d..f63c12d7dc 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 023b2bc46a..5335df00b9 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 1cf8320468..8e684718a0 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 3527a1c695..cdfe36cd3c 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 6746100ef2..9f179e9b1a 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 6a55f3cc71..5791fefc6a 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 75d15a89d6..0de5ffe6ea 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 3e63520ce2..a2681241c6 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 3a06449bd9..4d5b6bce44 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 9267d74dd2..5da25b24d0 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index b63ad8a1ac..9cc958cde9 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index d77303b40c..b069dbe413 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index f746b58b7d..2565016226 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 10ce5ede63..ca992646a9 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index 63f2247aba..82b7981f8f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.queue.util.TbTransportComponent; -import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -165,7 +164,6 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis static class CertificateWatcher { private final List paths; private final Runnable reloadCallback; - private final Map lastModifiedMap; private final Map lastChecksumMap; private int consecutiveFailures; private String failedCombinedChecksum; @@ -173,60 +171,23 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis CertificateWatcher(List paths, Runnable reloadCallback) { this.paths = paths; this.reloadCallback = reloadCallback; - this.lastModifiedMap = new HashMap<>(); this.lastChecksumMap = new HashMap<>(); for (Path path : paths) { - lastModifiedMap.put(path, getLastModifiedTime(path)); lastChecksumMap.put(path, calculateChecksum(path)); } this.consecutiveFailures = 0; } synchronized void checkAndReload(String name) { - boolean anyModifiedChanged = false; - for (Path path : paths) { - long currentModified = getLastModifiedTime(path); - Long lastModified = lastModifiedMap.getOrDefault(path, 0L); - if (currentModified != lastModified) { - anyModifiedChanged = true; - break; - } - } - if (!anyModifiedChanged) { - return; - } - - // Capture mtimes and checksums together before the callback runs. - // Pairing a post-callback mtime with a pre-callback checksum would let a write-during-reload be missed on the next poll. - Map currentModifiedTimes = new HashMap<>(); Map currentChecksums = new HashMap<>(); - StringBuilder combined = new StringBuilder(); for (Path path : paths) { - currentModifiedTimes.put(path, getLastModifiedTime(path)); - String checksum = calculateChecksum(path); - currentChecksums.put(path, checksum); - if (!combined.isEmpty()) { - combined.append("|"); - } - combined.append(path).append("=").append(checksum); + currentChecksums.put(path, calculateChecksum(path)); } - String combinedChecksum = combined.toString(); - - // Build old combined checksum for comparison - StringBuilder oldCombined = new StringBuilder(); - for (Path path : paths) { - if (!oldCombined.isEmpty()) { - oldCombined.append("|"); - } - oldCombined.append(path).append("=").append(lastChecksumMap.getOrDefault(path, "")); - } - String oldCombinedChecksum = oldCombined.toString(); + String combinedChecksum = combinedChecksum(currentChecksums); + String oldCombinedChecksum = combinedChecksum(lastChecksumMap); if (combinedChecksum.equals(oldCombinedChecksum)) { - // Content unchanged, just update modification times - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - } + // Content unchanged return; } @@ -237,41 +198,34 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis } if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { - // Update modification times to avoid re-checking mtime and re-computing checksums every poll cycle - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - } return; } try { log.info("Certificate change detected for: {}. Triggering reload...", name); reloadCallback.run(); - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - lastChecksumMap.put(path, currentChecksums.get(path)); - } + lastChecksumMap.putAll(currentChecksums); consecutiveFailures = 0; failedCombinedChecksum = null; } catch (Exception e) { consecutiveFailures++; failedCombinedChecksum = combinedChecksum; - // Deliberately NOT updating the lastModifiedMap here, so the next poll cycle retries - // (mtime mismatch passes the early gate, checksum matches failedCombinedChecksum). + // Deliberately NOT updating lastChecksumMap here, so the next poll cycle still sees a differing + // checksum, re-enters this method, and retries the same content. log.error("Failed to reload certificate for {} (attempt {}/{}): {}", name, consecutiveFailures, MAX_CONSECUTIVE_FAILURES, e.getMessage(), e); } } - private long getLastModifiedTime(Path path) { - try { - if (!Files.exists(path)) { - return 0; + private String combinedChecksum(Map checksums) { + StringBuilder combined = new StringBuilder(); + for (Path path : paths) { + if (!combined.isEmpty()) { + combined.append("|"); } - return Files.getLastModifiedTime(path).toMillis(); - } catch (IOException e) { - return 0; + combined.append(path).append("=").append(checksums.getOrDefault(path, "")); } + return combined.toString(); } private String calculateChecksum(Path path) { diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java index 6f78eaefae..8894937177 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -31,10 +31,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; public class CertificateReloadManagerTest { @@ -59,11 +56,10 @@ public class CertificateReloadManagerTest { } } - private void writeFileAndAwaitMtimeChange(Path path, String content, long baselineMtime) throws IOException { + private void writeFileAndBumpMtime(Path path, String content, long baselineMtime) throws IOException { Files.writeString(path, content); - await().atMost(2, SECONDS) - .pollInterval(10, MILLISECONDS) - .until(() -> Files.getLastModifiedTime(path).toMillis() != baselineMtime); + // Force a strictly newer mtime: back-to-back writes can share a millisecond, hiding the change from the watcher. + Files.setLastModifiedTime(path, FileTime.fromMillis(baselineMtime + 1000)); } private long mtime(Path path) throws IOException { @@ -77,7 +73,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -85,7 +81,7 @@ public class CertificateReloadManagerTest { } @Test - public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { + public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() { AtomicInteger reloadCount = new AtomicInteger(0); certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); @@ -147,7 +143,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadCount::incrementAndGet); long baseline = mtime(keyFile); - writeFileAndAwaitMtimeChange(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline); + writeFileAndBumpMtime(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -168,8 +164,8 @@ public class CertificateReloadManagerTest { long baseline1 = mtime(certFile); long baseline2 = mtime(cert2File); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); - writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -191,8 +187,8 @@ public class CertificateReloadManagerTest { long baseline1 = mtime(certFile); long baseline2 = mtime(cert2File); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); - writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -224,9 +220,7 @@ public class CertificateReloadManagerTest { for (int i = 0; i < 5; i++) { Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n"); } - await().atMost(2, SECONDS) - .pollInterval(10, MILLISECONDS) - .until(() -> mtime(certFile) != baseline); + Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline + 1000)); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -242,7 +236,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 5; i++) { new Thread(() -> { @@ -272,7 +266,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, originalContent, baseline); + writeFileAndBumpMtime(certFile, originalContent, baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -289,7 +283,7 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -311,19 +305,41 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(1); shouldFail.set(0); long baseline2 = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(2); } + @Test + public void givenContentChangedButMtimeUnchanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception { + // Bug fingerprint: a cert-manager rotation that lands in the same wall-clock millisecond as the + // watcher's recorded baseline mtime. Files.getLastModifiedTime().toMillis() truncates to the ms, + // so the rotated content shares the baseline mtime and an mtime-only gate would never re-hash it. + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + long baseline = mtime(certFile); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nROTATED_SAME_MS\n-----END CERTIFICATE-----\n"); + // Force the mtime back to the exact baseline millisecond — content changed, timestamp did not. + Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline)); + + // Sanity guard: the watcher observes a timestamp identical to its recorded baseline. + assertThat(mtime(certFile)).isEqualTo(baseline); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + assertThat(reloadCount.get()).isEqualTo(1); + } + @Test public void givenCallbackHitMaxFailures_whenFileChangesToNewContent_thenShouldResetAndRetry() throws Exception { AtomicInteger reloadAttempts = new AtomicInteger(0); @@ -337,7 +353,7 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -346,7 +362,7 @@ public class CertificateReloadManagerTest { shouldFail.set(0); long baseline2 = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(11); diff --git a/common/util/pom.xml b/common/util/pom.xml index 4748d75d92..3c60d4f077 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 66c87eb5d1..09c4318a20 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index bc172eeb45..4449502857 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 9bf82c7a65..2d811909ec 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 2601ac47fe..fa8b83c6f7 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index c06e98b45e..e433d6bfb2 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index f85e9b7691..54a1cd91c7 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 9cd29e35d5..070bf829a5 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.2.2", + "version": "4.2.2.3", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 2f7434b472..7174947cbc 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index ab484e7df8..519c928b6e 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 84c98308cd..2552bf188c 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 3dd84135cb..ba83cbb211 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 5c10adf67e..a20505772e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f081e6fd46..309536b4ef 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 78e445fda8..f7ea4160d1 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index d65d07f9a9..d80b63217c 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 8dfe14b3a6..0bf990171b 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index aa10ff1184..a711411729 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index fb344e3242..f0996dc1bf 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 0bc90c6ac4..9d781e0854 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index a0c023b5f8..47fff0501d 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index aaac7f8b67..4c58d7a94f 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.2.2", + "version": "4.2.2.3", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 196311f880..bc40e149ff 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 4b9b9fa21e..6a2b55cd84 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard netty-mqtt - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 4a8958ffa3..639fc2ab17 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT pom Thingsboard @@ -69,7 +69,7 @@ 3.5.13 3.18.0 42.7.11 - 4.1.133.Final + 4.1.134.Final 10.1.55 2.4.0-b180830.0359 0.12.5 diff --git a/rest-client/pom.xml b/rest-client/pom.xml index d108c49cda..9051482016 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index de69670863..2c73670dde 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 3810c46a99..8d9b6e5cbc 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 1ac0938163..dcb0f975f2 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 6376db6d9f..137dbaa1a7 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 723f9a1dd2..4fef3b50e7 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index d4e19705c9..8d24e0d982 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index fac60f7edc..99b98fa7c9 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 7b61c03200..1d61b07440 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 3645cf7d73..3889d8d334 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 683c0e3e2d..a1f9d384f1 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 48e81c9960..b2f7276532 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 4bc4bfd6cd..863463e78c 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 5572730923..9ef4a4461b 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/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; diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index b092f7dc5f..9adc5cdce7 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -17,6 +17,7 @@ import { AuthUser, User } from '@shared/models/user.model'; import { UserSettings } from '@shared/models/user-settings.models'; import { TrendzSettings } from '@shared/models/trendz-settings.models'; +import { NullsOrderStrategy } from '@shared/models/page/page-link'; export interface SysParamsState { userTokenAccessEnabled: boolean; @@ -34,6 +35,8 @@ export interface SysParamsState { ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; trendzSettings: TrendzSettings; + nullsOrderStrategy: NullsOrderStrategy; + edqsEnabled: boolean; iotHubBaseUrl: string; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 5838bee5c6..c3afbd5e76 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/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: '' }; diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 1c6ac90f3b..744cb92534 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -20,6 +20,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { setEdqsEnabled, setNullsOrderStrategy } from '@shared/models/page/page-link'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; import { Router, UrlTree } from '@angular/router'; @@ -441,6 +442,8 @@ export class AuthService { return this.http.get('/api/system/params', defaultHttpOptions()).pipe( map((sysParams) => { this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit); + setNullsOrderStrategy(sysParams.nullsOrderStrategy); + setEdqsEnabled(sysParams.edqsEnabled); return sysParams; }), catchError(() => of({} as SysParamsState)) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html index 72f4b0f1d8..500d974726 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
widget-config.actions
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts index 0320dc7fd1..389c780e4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts +++ b/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) => {}; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html index 298bdb6e61..907da3e14b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html @@ -17,4 +17,8 @@ --> + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts index d057acf057..3872e07853 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 31927e0cb3..711107895f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/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 { private appliedPageLink: AlarmDataPageLink; private appliedSortOrderLabel: string; + private appliedSortColumnType: SortColumnType = 'entityField'; private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; @@ -1294,11 +1300,13 @@ class AlarmsDatasource implements DataSource { 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 { } 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); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index b99ba01212..8bd5f07896 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/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 { private appliedPageLink: EntityDataPageLink; private appliedSortOrderLabel: string; + private appliedSortColumnType: SortColumnType = 'entityField'; private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; @@ -905,11 +910,13 @@ class EntityDatasource implements DataSource { 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 { }); 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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts index f7bae926bc..51ee9871f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts @@ -15,8 +15,10 @@ /// import { - Component, + ChangeDetectorRef, + Component, ComponentRef, ElementRef, + inject, Inject, Injector, Input, @@ -96,6 +98,7 @@ export class HtmlContainerWidgetComponent implements OnInit { @Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService, private utils: UtilsService, + private cdr: ChangeDetectorRef, private resources: ResourcesService) {} ngOnInit(): void { @@ -160,11 +163,13 @@ export class HtmlContainerWidgetComponent implements OnInit { this.compileAngularFunction().subscribe( { next: (containerFunction) => { - try { - this.initAngularComponent(imports, containerFunction); - } catch (e) { - this.handleWidgetException(e); - } + setTimeout(() => { + try { + this.initAngularComponent(imports, containerFunction); + } catch (e) { + this.handleWidgetException(e); + } + }); }, error: (e) => { this.handleWidgetException(e); @@ -200,9 +205,16 @@ export class HtmlContainerWidgetComponent implements OnInit { compileModules = compileModules.concat(imports); } const self = () => this; + + let containerRef: ComponentRef; + this.dynamicComponentFactoryService.createDynamicComponent( class TbContainerInstance { + + private cdr = inject(ChangeDetectorRef); + ngOnInit(): void { + this.cdr.detach(); if (containerFunction) { const instance = self(); try { @@ -212,6 +224,15 @@ export class HtmlContainerWidgetComponent implements OnInit { } } } + ngDoCheck(): void { + const instance = self(); + try { + this.cdr.detectChanges() + } catch (error) { + containerRef.destroy(); + instance.handleWidgetException(error) + } + } ngOnDestroy(): void { destroyContainerInstanceResources(); } @@ -224,7 +245,7 @@ export class HtmlContainerWidgetComponent implements OnInit { this.containerInstanceComponentType = componentType; const injector: Injector = Injector.create({providers: [], parent: this.angularContainer.viewContainerRef.injector}); try { - this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType, + containerRef = this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType, {index: 0, injector}); } catch (error) { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index cb96a493a1..fc7ad9f64b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -276,6 +276,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, elementClick: this.elementClick.bind(this), cardClick: this.cardClick.bind(this), click: this.click.bind(this), + invokeAction: this.invokeAction.bind(this), getActiveEntityInfo: this.getActiveEntityInfo.bind(this), openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this), openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this), @@ -1590,6 +1591,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } + private invokeAction($event: Event, actionName: string, additionalParams?: any) { + const descriptors = this.getActionDescriptors('javaScript'); + if (descriptors?.length) { + const found = descriptors.find(d => d.name === actionName); + if (found) { + this.handleWidgetAction($event, found, null, null, additionalParams); + } + } + } + private onWidgetAction($event: Event, action: WidgetAction) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 1840bcce9d..d72587812c 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -149,6 +149,8 @@ export interface IDashboardWidget { updateWidgetParams(): void; } +export type WidgetDestroyCallback = () => void; + export class WidgetContext { constructor(public dashboard: IDashboardComponent, @@ -343,6 +345,8 @@ export class WidgetContext { ...RxJSOperators }; + private destroyCallbacks: WidgetDestroyCallback[] = []; + registerPopoverComponent(popoverComponent: TbPopoverComponent) { this.popoverComponents.push(popoverComponent); popoverComponent.tbDestroy.subscribe(() => { @@ -382,6 +386,10 @@ export class WidgetContext { } } + registerDestroyCallback(destroyCallback: WidgetDestroyCallback) { + this.destroyCallbacks.push(destroyCallback); + } + showSuccessToast(message: string, duration: number = 1000, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', @@ -494,6 +502,13 @@ export class WidgetContext { labelPattern.destroy(); } this.labelPatterns.clear(); + this.destroyCallbacks.forEach((destroyCallback) => { + try { + destroyCallback() + } catch (_ignoredError) { /* empty */ } + } + ); + this.destroyCallbacks.length = 0; this.width = undefined; this.height = undefined; this.destroyed = true; diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts index e6ce4b3ef7..757677fadd 100644 --- a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -632,6 +632,28 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi optional: true } ] + }, + invokeAction: { + description: 'Invoke action with JavaScript action source.', + meta: 'function', + args: [ + { + name: '$event', + description: 'DOM event object associated with action.', + type: 'Event' + }, + { + name: 'actionName', + description: 'Name of the configured action with JavaScript action source.', + type: 'string' + }, + { + name: 'additionalParams', + description: 'Optional payload merged into the action context and forwarded to the configured JavaScript action function as its additionalParams argument. Use it to pass row data, button state, or any extra values the action handler should react to.', + type: 'object', + optional: true + } + ] } } }, @@ -736,6 +758,17 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi } } } + }, + registerDestroyCallback: { + description: 'Registers a teardown callback that will be invoked exactly once when the widget is about to be destroyed (dashboard navigation, edit/view switch, layout change, etc.). Use it to release resources acquired during widget setup so they don\'t leak across widget reloads — e.g. unsubscribe RxJS subscriptions, detach DOM/window event listeners, clear setInterval / setTimeout timers, abort outstanding HTTP requests, destroy third-party plugin instances. Multiple callbacks may be registered; they are executed in registration order.', + meta: 'function', + args: [ + { + description: 'Zero-argument function executed when the widget is destroyed. Should be idempotent — synchronously dispose of one specific resource (e.g. one subscription or one listener) and avoid throwing; throwing here may prevent later cleanup callbacks from running.', + name: 'destroyCallback', + type: '() => void', + } + ] } }, ...serviceCompletions diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index eb2c059c2a..f3837a5cc5 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -22,6 +22,19 @@ import { EntitiesTableAction } from '@home/models/entity/entity-table-component. export const MAX_SAFE_PAGE_SIZE = 2147483647; +export type NullsOrderStrategy = 'default' | 'nulls_first' | 'nulls_last'; + +let nullsOrderStrategy: NullsOrderStrategy = 'default'; +let edqsEnabled = false; + +export function setNullsOrderStrategy(value: NullsOrderStrategy): void { + nullsOrderStrategy = value ?? 'default'; +} + +export function setEdqsEnabled(value: boolean): void { + edqsEnabled = !!value; +} + export type PageLinkSearchFunction = (entity: T, textSearch: string, searchProperty?: string) => boolean; export interface PageQueryParam extends Partial{ @@ -73,9 +86,37 @@ const defaultPageLinkSearch: PageLinkSearchFunction = return false; }; -export function sortItems(item1: any, item2: any, property: string, asc: boolean): number { +export type SortColumnType = 'entityField' | 'attribute' | 'timeseries'; + +export function sortItems(item1: any, item2: any, property: string, asc: boolean, + columnType: SortColumnType = 'entityField'): number { const item1Value = getDescendantProp(item1, property); const item2Value = getDescendantProp(item2, property); + const item1Empty = item1Value === null || item1Value === undefined || item1Value === ''; + const item2Empty = item2Value === null || item2Value === undefined || item2Value === ''; + // Mirror backend's nulls ordering. EDQS uses fixed NULLS FIRST regardless of strategy and + // naive compare below already matches it, so skip this branch when EDQS is on. + // For entityField columns the ORDER BY hits a real nullable DB column → strategy always applies. + // For attribute/timeseries the strategy only applies to numeric/boolean values; string/json + // are coalesced to '' on the backend, so naive compare below already matches its order. + if (!edqsEnabled && (item1Empty || item2Empty) && !(item1Empty && item2Empty)) { + let applyStrategy = columnType === 'entityField'; + if (!applyStrategy) { + const other = item1Empty ? item2Value : item1Value; + applyStrategy = + typeof other === 'boolean' || other === 'true' || other === 'false' || + (typeof other === 'number' && isFinite(other)) || + (typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other))); + } + if (applyStrategy) { + const nullsFirst = nullsOrderStrategy === 'nulls_first' + || (nullsOrderStrategy === 'default' && !asc); + if (item1Empty) { + return nullsFirst ? -1 : 1; + } + return nullsFirst ? 1 : -1; + } + } let result = 0; if (item1Value !== item2Value) { const item1Type = typeof item1Value; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index e13220ff76..9f56c5aaf4 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -987,14 +987,14 @@ "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-modules-systemjs@^7.27.1": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" - integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== + version "7.29.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20" + integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w== dependencies: - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.5" + "@babel/traverse" "^7.29.0" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" @@ -1298,7 +1298,7 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6": +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== @@ -4349,17 +4349,17 @@ boolbase@^1.0.0: integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: - version "1.1.13" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6" - integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== + version "1.1.14" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b" + integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" - integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + version "5.0.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" + integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== dependencies: balanced-match "^4.0.2" @@ -6113,9 +6113,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec" + integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ== fastq@^1.6.0: version "1.17.1"