From 57024c223e7ef682fdb6c2cc4a89767c4186f4b0 Mon Sep 17 00:00:00 2001 From: Paolo Cristiani <42511852+pgrisu@users.noreply.github.com> Date: Tue, 13 May 2025 14:44:27 +0200 Subject: [PATCH 001/121] feat: add tooltip stacked total option in time series chart settings --- ...e-series-chart-basic-config.component.html | 5 +++ ...ime-series-chart-basic-config.component.ts | 10 ++++- .../chart/time-series-chart-tooltip.models.ts | 45 ++++++++++++++++++- .../lib/chart/time-series-chart.models.ts | 1 + ...eries-chart-widget-settings.component.html | 5 +++ ...-series-chart-widget-settings.component.ts | 9 +++- .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 61c9f89c7c..8d1b5eaad7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -311,6 +311,11 @@ +
+ + {{ 'tooltip.show-stack-total' | translate }} + +
{{ 'tooltip.background-color' | translate }}
+
+ + {{ 'tooltip.show-stack-total' | translate }} + +
{{ 'tooltip.background-color' | translate }}
Date: Wed, 14 May 2025 18:11:20 +0200 Subject: [PATCH 002/121] Add show-total legend setting to latest-chart widgets --- .../basic/chart/latest-chart-basic-config.component.html | 3 +++ .../basic/chart/latest-chart-basic-config.component.ts | 4 ++++ .../components/widget/lib/chart/latest-chart.component.ts | 5 ++++- .../home/components/widget/lib/chart/latest-chart.models.ts | 2 ++ .../chart/latest-chart-widget-settings.component.html | 3 +++ .../settings/chart/latest-chart-widget-settings.component.ts | 3 +++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index 4a9569fb29..4601eaa4f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -143,6 +143,9 @@
+ + {{ 'legend.show-total' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts index 9890895938..1a75f95d2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts @@ -176,6 +176,7 @@ export abstract class LatestChartBasicConfigComponent !item.total); } legendLabelStyle: ComponentStyle; @@ -92,6 +93,7 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private shapeResize$: ResizeObserver; private legendHorizontal: boolean; + private legendShowTotal: boolean; private latestChart: TbLatestChart; @@ -119,6 +121,7 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { this.legendValueStyle = textStyle(this.settings.legendValueFont); this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); this.legendValueStyle.color = this.settings.legendValueColor; + this.legendShowTotal = this.settings.legendShowTotal; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts index 931d3c52ed..9184bc59e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts @@ -103,6 +103,7 @@ export interface LatestChartWidgetSettings extends LatestChartSettings { legendLabelColor: string; legendValueFont: Font; legendValueColor: string; + legendShowTotal: boolean; background: BackgroundSettings; padding: string; } @@ -129,6 +130,7 @@ export const latestChartWidgetDefaultSettings: LatestChartWidgetSettings = { lineHeight: '20px' }, legendValueColor: 'rgba(0, 0, 0, 0.87)', + legendShowTotal: true, background: { type: BackgroundType.color, color: '#fff', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index 04476e69e2..5246747c20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -61,6 +61,9 @@ + + {{ 'legend.show-total' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts index 1d7baebd33..b67a1d85f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts @@ -134,6 +134,7 @@ export abstract class LatestChartWidgetSettingsComponent Date: Tue, 29 Jul 2025 14:30:59 +0300 Subject: [PATCH 003/121] added json timeseries type for bulk import --- .../csv/AbstractBulkImportService.java | 9 ++- .../thingsboard/server/utils/CsvUtils.java | 19 ++++++ .../controller/DeviceControllerTest.java | 60 +++++++++++++++++++ .../server/common/data/util/TypeCastUtil.java | 11 ++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 9850e2d1a1..768adf98ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import jakarta.annotation.Nullable; @@ -183,7 +184,13 @@ public abstract class AbstractBulkImportService dataEntry.getKey().getType() == kvType && StringUtils.isNotEmpty(dataEntry.getKey().getKey())) - .forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive())); + .forEach(dataEntry -> { + ParsedValue value = dataEntry.getValue(); + JsonElement kvValue = (value.getDataType() == DataType.JSON) + ? (JsonElement) value.getValue() + : value.toJsonPrimitive(); + kvs.add(dataEntry.getKey().getKey(), kvValue); + }); return Map.entry(kvType, kvs); }) .filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) diff --git a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java index 3f75a4918f..0fde72a3b0 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java @@ -17,10 +17,15 @@ package org.thingsboard.server.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.SneakyThrows; import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.input.CharSequenceReader; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,4 +48,18 @@ public class CsvUtils { .collect(Collectors.toList()); } + @SneakyThrows + public static byte[] generateCsv(List> rows) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); + CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) { + + for (List row : rows) { + csvPrinter.printRecord(row); + } + csvPrinter.flush(); + } + return out.toByteArray(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 36cede9e84..6b31825640 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -77,8 +77,13 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.utils.CsvUtils; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -1586,6 +1591,61 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(newAttributeValue, actualAttribute.get("value")); } + @Test + public void testBulkImportDeviceWithJsonAttr() throws Exception { + String deviceName = "some_device"; + String deviceType = "some_type"; + JsonNode deviceAttr = JacksonUtil.toJsonNode("{\"threshold\": 45}"); + + List> content = new LinkedList<>(); + content.add(Arrays.asList("NAME", "TYPE", "ATTR")); + content.add(Arrays.asList(deviceName, deviceType, deviceAttr.toString())); + + byte[] bytes = CsvUtils.generateCsv(content); + BulkImportRequest request = new BulkImportRequest(); + request.setFile(new String(bytes, StandardCharsets.UTF_8)); + BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping(); + BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping(); + name.setType(BulkImportColumnType.NAME); + BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping(); + type.setType(BulkImportColumnType.TYPE); + BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping(); + attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE); + attr.setKey("attr"); + List columns = new ArrayList<>(); + columns.add(name); + columns.add(type); + columns.add(attr); + + mapping.setColumns(columns); + mapping.setDelimiter(','); + mapping.setUpdate(true); + mapping.setHeader(true); + request.setMapping(mapping); + + BulkImportResult deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + + Assert.assertEquals(1, deviceBulkImportResult.getCreated().get()); + Assert.assertEquals(0, deviceBulkImportResult.getErrors().get()); + Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get()); + Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); + + Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + + Assert.assertNotNull(savedDevice); + Assert.assertEquals(deviceName, savedDevice.getName()); + Assert.assertEquals(deviceType, savedDevice.getType()); + + //check server attribute value + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Map actualAttribute = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference>>() {}).stream() + .filter(att -> att.get("key").equals("attr")).findFirst().get(); + LinkedHashMap expected = JacksonUtil.convertValue(deviceAttr, new TypeReference<>() {}); + Assert.assertEquals(expected, actualAttribute.get("value")); + }); + } + @Test public void testSaveDeviceWithOutdatedVersion() throws Exception { Device device = createDevice("Device v1.0"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java index 85071b3cc9..94a1ee0ef4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.util; +import com.google.gson.JsonParser; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.kv.DataType; @@ -40,6 +41,11 @@ public class TypeCastUtil { } catch (RuntimeException ignored) {} } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value)); + } else if (looksLikeJson(value)) { + try { + return Pair.of(DataType.JSON, JsonParser.parseString(value)); + } catch (Exception ignored) { + } } return Pair.of(DataType.STRING, value); } @@ -70,4 +76,9 @@ public class TypeCastUtil { return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e"); } + private static boolean looksLikeJson(String value) { + return (value.startsWith("{") && value.endsWith("}")) || + (value.startsWith("[") && value.endsWith("]")); + } + } From 6ae8cdf62389d7505c79110036558c9a3191c478 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 30 Jul 2025 18:38:00 +0300 Subject: [PATCH 004/121] added trim to json parsing, updated test --- .../controller/DeviceControllerTest.java | 19 ++++++++----------- .../server/common/data/util/TypeCastUtil.java | 5 +++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 6b31825640..6ac9e5526a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration; import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -82,10 +84,10 @@ import org.thingsboard.server.utils.CsvUtils; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -1595,11 +1597,11 @@ public class DeviceControllerTest extends AbstractControllerTest { public void testBulkImportDeviceWithJsonAttr() throws Exception { String deviceName = "some_device"; String deviceType = "some_type"; - JsonNode deviceAttr = JacksonUtil.toJsonNode("{\"threshold\": 45}"); + String deviceAttr = "{\"threshold\":45}"; List> content = new LinkedList<>(); content.add(Arrays.asList("NAME", "TYPE", "ATTR")); - content.add(Arrays.asList(deviceName, deviceType, deviceAttr.toString())); + content.add(Arrays.asList(deviceName, deviceType, deviceAttr)); byte[] bytes = CsvUtils.generateCsv(content); BulkImportRequest request = new BulkImportRequest(); @@ -1636,14 +1638,9 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(deviceName, savedDevice.getName()); Assert.assertEquals(deviceType, savedDevice.getType()); - //check server attribute value - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - Map actualAttribute = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() + - "/values/attributes/SERVER_SCOPE", new TypeReference>>() {}).stream() - .filter(att -> att.get("key").equals("attr")).findFirst().get(); - LinkedHashMap expected = JacksonUtil.convertValue(deviceAttr, new TypeReference<>() {}); - Assert.assertEquals(expected, actualAttribute.get("value")); - }); + Optional retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get(); + assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr); + assertThat(retrieved.get().getStrValue()).isNotPresent(); } @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java index 94a1ee0ef4..82de579d3d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java @@ -77,8 +77,9 @@ public class TypeCastUtil { } private static boolean looksLikeJson(String value) { - return (value.startsWith("{") && value.endsWith("}")) || - (value.startsWith("[") && value.endsWith("]")); + String trimmed = value.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")); } } From e30adf0447450a6e2796392fd5b06e06bdba6c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=97=AD?= Date: Sun, 10 Aug 2025 10:30:49 +0800 Subject: [PATCH 005/121] Fix: Improve Edge session cleanup to prevent resource leaks In unstable network environments, Edge devices may frequently disconnect and reconnect. The previous session cleanup logic could fail to stop the Kafka consumer, creating a 'zombie consumer'. This commit introduces a multi-layered defense: 1. Proactively evicts stale members from the Kafka consumer group upon new connection to ensure immediate functionality. 2. Adds a background task to persistently try and clean up session objects that failed to destroy, preventing memory/thread leaks. --- .../service/edge/rpc/EdgeGrpcService.java | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index eaef1f7c7d..e1a193bdfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -69,17 +69,8 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -102,6 +93,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); + private final Queue zombieSessions = new ConcurrentLinkedQueue<>(); @Value("${edges.rpc.port}") private int rpcPort; @@ -166,6 +158,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private ScheduledExecutorService executorService; + private ScheduledExecutorService zombieSessionsExecutorService; + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void onStartUp() { log.info("Initializing Edge RPC service!"); @@ -197,7 +191,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); + this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); + this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -520,11 +516,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i edgeIdServiceIdCache.evict(edgeId); } - private void destroySession(EdgeGrpcSession session) { + private boolean destroySession(EdgeGrpcSession session) { try (session) { for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { if (session.destroy()) { - break; + return true; } else { try { Thread.sleep(100); @@ -532,6 +528,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } } + return false; } private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { @@ -663,4 +660,28 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.warn("Failed to cleanup kafka sessions", e); } } + + private void cleanupZombieSessions() { + int zombiesToProcess = zombieSessions.size(); + if (zombiesToProcess == 0) { + return; + } + log.info("Found {} zombie sessions in the queue. Starting cleanup cycle.", zombiesToProcess); + for (int i = 0; i < zombiesToProcess; i++) { + EdgeGrpcSession zombie = zombieSessions.poll(); + if (zombie == null) { + break; + } + log.warn("[{}] Attempting to clean up zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getSessionId(), zombie.getEdge().getId()); + if (!destroySession(zombie)) { + log.warn("[{}] Zombie session [{}] cleanup failed again. Re-queuing for next attempt.", + zombie.getTenantId(), zombie.getSessionId()); + zombieSessions.add(zombie); + } else { + log.info("[{}] Successfully cleaned up zombie session [{}].", + zombie.getTenantId(), zombie.getSessionId()); + } + } + } } From 4523efbcdd73993c3822f68e587ac33157f663e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=97=AD?= Date: Sun, 10 Aug 2025 11:42:47 +0800 Subject: [PATCH 006/121] Fix: Improve Edge session cleanup to prevent resource leaks In unstable network environments, Edge devices may frequently disconnect and reconnect. The previous session cleanup logic could fail to stop the Kafka consumer, creating a 'zombie consumer'. This commit introduces a multi-layered defense: 1. Proactively evicts stale members from the Kafka consumer group upon new connection to ensure immediate functionality. 2. Adds a background task to persistently try and clean up session objects that failed to destroy, preventing memory/thread leaks. --- .../thingsboard/server/service/edge/rpc/EdgeGrpcService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index e1a193bdfa..4217fd509b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -219,6 +219,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (executorService != null) { executorService.shutdownNow(); } + if(zombieSessionsExecutorService != null){ + zombieSessionsExecutorService.shutdownNow(); + } } @Override From b2ae6f92d12206ea185a2e882945a6b69234bf03 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 27 Aug 2025 16:05:20 +0300 Subject: [PATCH 007/121] added Content-Security-Policy header to download image api to prevent malicious code injection --- .../java/org/thingsboard/server/controller/ImageController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index f9ec7fd844..9288484f86 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -300,6 +300,7 @@ public class ImageController extends BaseController { tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() .header("Content-Type", descriptor.getMediaType()) + .header("Content-Security-Policy", "default-src 'none'") .eTag(descriptor.getEtag()); if (!cacheKey.isPublic()) { result From 5b14dc67fccb0c6987223371ffad49043761353d Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Mon, 1 Sep 2025 14:12:34 +0300 Subject: [PATCH 008/121] Added feature to upload dashboard JSON file to update --- .../dashboard/dashboard-form.component.html | 13 +++++++++++++ .../pages/dashboard/dashboard-form.component.ts | 14 ++++++++++++++ .../dashboards-table-config.resolver.ts | 16 +++++++++++++++- ui-ngx/src/app/shared/models/dashboard.models.ts | 1 + .../src/assets/locale/locale.constant-en_US.json | 1 + 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 379e37528f..4d29376ccd 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -144,6 +144,19 @@ formControlName="image"> + +
+
dashboard.update-dashboard
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index c8174013aa..a612d2b127 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -45,6 +45,7 @@ export class DashboardFormComponent extends EntityComponent { publicLink: string; assignedCustomersText: string; entityType = EntityType; + currentFileName: string = ''; constructor(protected store: Store, protected translate: TranslateService, @@ -84,6 +85,7 @@ export class DashboardFormComponent extends EntityComponent { { title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], image: [entity ? entity.image : null], + fileContent: [null], mobileHide: [entity ? entity.mobileHide : false], mobileOrder: [entity ? entity.mobileOrder : null, [Validators.pattern(/^-?[0-9]+$/)]], configuration: this.fb.group( @@ -101,9 +103,11 @@ export class DashboardFormComponent extends EntityComponent { } updateForm(entity: Dashboard) { + this.currentFileName = ''; this.updateFields(entity); this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({image: entity.image}); + this.entityForm.patchValue({fileContent: entity.fileContent || null}); this.entityForm.patchValue({mobileHide: entity.mobileHide}); this.entityForm.patchValue({mobileOrder: entity.mobileOrder}); this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); @@ -143,4 +147,14 @@ export class DashboardFormComponent extends EntityComponent { this.publicLink = this.dashboardService.getPublicDashboardLink(entity); } } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index b37c2731c6..4540fb9af1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -110,7 +110,7 @@ export class DashboardsTableConfigResolver { this.config.deleteEntitiesContent = () => this.translate.instant('dashboard.delete-dashboards-text'); this.config.loadEntity = id => this.dashboardService.getDashboard(id.id); - this.config.saveEntity = dashboard => this.saveAndAssignDashboard(dashboard as DashboardSetup); + this.config.saveEntity = dashboard => this.saveAndAssignDashboard(this.dashboardContentModification(dashboard) as DashboardSetup); this.config.onEntityAction = action => this.onDashboardAction(action); this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user'); @@ -179,6 +179,20 @@ export class DashboardsTableConfigResolver { ); } + private dashboardContentModification(dashboard: Dashboard): Dashboard{ + if(dashboard.fileContent != undefined){ + const { description, ...dashboardContent } = dashboard.fileContent; + + dashboard.configuration = { + ...dashboard.configuration, + ...dashboardContent + } + } + delete dashboard.fileContent; + + return dashboard; + } + configureColumns(dashboardScope: string): Array> { const columns: Array> = [ new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 8fe92f1b80..acd7e63b68 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -195,6 +195,7 @@ export interface Dashboard extends DashboardInfo { configuration?: DashboardConfiguration; dialogRef?: MatDialogRef; resources?: Array; + fileContent?: DashboardConfiguration; } export interface HomeDashboard extends Dashboard { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fd368e2853..962d0867a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1347,6 +1347,7 @@ "mobile-order": "Dashboard order in mobile application", "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", + "update-dashboard": "Update the dashboard", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", From e4588616f36b251a6c0ac17b96c7e0e0ed0775df Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 10:34:56 +0300 Subject: [PATCH 009/121] Updated logic for file upload, changed to dialog window upload --- .../dashboard/dashboard-form.component.html | 19 +--- .../dashboard/dashboard-form.component.ts | 19 +--- .../home/pages/dashboard/dashboard.module.ts | 4 +- .../dashboards-table-config.resolver.ts | 104 +++++++++-------- ...mport-dashboard-file-dialog.component.html | 73 ++++++++++++ .../import-dashboard-file-dialog.component.ts | 106 ++++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 4d29376ccd..2e185d66ad 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -28,6 +28,12 @@ [class.!hidden]="isEdit || dashboardScope !== 'tenant'"> {{'dashboard.export' | translate }} + + + + +
+
+ + +
+ +
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts new file mode 100644 index 0000000000..4cfa5a06ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -0,0 +1,106 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2025 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; +import {DashboardService} from '@core/http/dashboard.service'; +import {Dashboard, DashboardInfo} from '@app/shared/models/dashboard.models'; +import {ActionNotificationShow} from '@core/notification/notification.actions'; +import {TranslateService} from '@ngx-translate/core'; +import {DialogComponent} from '@shared/components/dialog.component'; +import {Router} from '@angular/router'; + +export interface DashboardInfoDialogData { + dashboard: Dashboard; +} + +@Component({ + selector: 'tb-import-dashboard-file-dialog', + templateUrl: './import-dashboard-file-dialog.component.html', + styleUrls: [] +}) +export class ImportDashboardFileDialogComponent extends DialogComponent implements OnInit { + + dashboard: Dashboard; + currentFileName: string = ''; + uploadFileFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, + public translate: TranslateService, + private dashboardService: DashboardService, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + this.dashboard = data.dashboard; + } + + ngOnInit(): void { + this.uploadFileFormGroup = this.fb.group({ + file: [null] + }); + } + + cancel(): void { + this.dialogRef.close(); + } + + save(){ + const fileControl = this.uploadFileFormGroup.get('file'); + if(!fileControl || !fileControl.value){ + return; + } + + const dashboardContent = { + ...fileControl.value, + description: this.dashboard.configuration.description + }; + this.dashboard.configuration = dashboardContent; + + this.dashboardService.saveDashboard(this.dashboard).subscribe(()=>{ + this.dialogRef.close(true); + }) + } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 962d0867a9..d8ab1ebc0b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1348,6 +1348,7 @@ "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", "update-dashboard": "Update the dashboard", + "upload-file-to-update": "Upload file to update", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", From 6f91d8dd1cee45ca00313a0a3558070c3d4773a2 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 10:40:52 +0300 Subject: [PATCH 010/121] Updated license --- ...mport-dashboard-file-dialog.component.html | 36 ++++++------------- .../import-dashboard-file-dialog.component.ts | 35 ++++++------------ 2 files changed, 20 insertions(+), 51 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html index b91b6627cc..5881d59328 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html @@ -1,36 +1,20 @@ -

{{ 'dashboard.update-dashboard' | translate }}

diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts index 4cfa5a06ab..fda1662f46 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -1,32 +1,17 @@ /// -/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// Copyright © 2016-2025 The Thingsboard Authors /// -/// Copyright © 2016-2025 ThingsBoard, Inc. All Rights Reserved. +/// 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 /// -/// NOTICE: All information contained herein is, and remains -/// the property of ThingsBoard, Inc. and its suppliers, -/// if any. The intellectual and technical concepts contained -/// herein are proprietary to ThingsBoard, Inc. -/// and its suppliers and may be covered by U.S. and Foreign Patents, -/// patents in process, and are protected by trade secret or copyright law. +/// http://www.apache.org/licenses/LICENSE-2.0 /// -/// Dissemination of this information or reproduction of this material is strictly forbidden -/// unless prior written permission is obtained from COMPANY. -/// -/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, -/// managers or contractors who have executed Confidentiality and Non-disclosure agreements -/// explicitly covering such access. -/// -/// The copyright notice above does not evidence any actual or intended publication -/// or disclosure of this source code, which includes -/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. -/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, -/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT -/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, -/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. -/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION -/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, -/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// 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, OnInit} from '@angular/core'; From 2fa1d234b3048e923d274ffd4b85d13fd099b3d7 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 16:58:55 +0300 Subject: [PATCH 011/121] Refactored formatting --- .../dashboard/dashboard-form.component.ts | 4 +- .../dashboards-table-config.resolver.ts | 50 +++++++++---------- .../import-dashboard-file-dialog.component.ts | 36 +++++++------ .../src/app/shared/models/dashboard.models.ts | 1 - 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index eca7b61dcc..844e0822a1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -31,7 +31,7 @@ import { DashboardService } from '@core/http/dashboard.service'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { isEqual } from '@core/utils'; import { EntityType } from '@shared/models/entity-type.models'; -import {PageLink} from "@shared/models/page/page-link"; +import { PageLink } from "@shared/models/page/page-link"; @Component({ selector: 'tb-dashboard-form', @@ -118,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent implements OnInit { - dashboard: Dashboard; + private dashboard: Dashboard; currentFileName: string = ''; - uploadFileFormGroup: UntypedFormGroup; + uploadFileFormGroup: FormGroup; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, - public translate: TranslateService, private dashboardService: DashboardService, - public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + protected dialogRef: MatDialogRef, + public fb: FormBuilder) { super(store, router, dialogRef); this.dashboard = data.dashboard; } @@ -62,9 +60,9 @@ export class ImportDashboardFileDialogComponent extends DialogComponent{ + this.dashboardService.saveDashboard(this.dashboard).subscribe(() => { this.dialogRef.close(true); }) } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index acd7e63b68..8fe92f1b80 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -195,7 +195,6 @@ export interface Dashboard extends DashboardInfo { configuration?: DashboardConfiguration; dialogRef?: MatDialogRef; resources?: Array; - fileContent?: DashboardConfiguration; } export interface HomeDashboard extends Dashboard { From 1754f29df79879dc3bcc1de6c573c202dac7f17e Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 17:01:10 +0300 Subject: [PATCH 012/121] Refactored formatting --- .../home/pages/dashboard/dashboard.module.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts index 99140bf7b7..5604d7e4b3 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -19,12 +19,16 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; -import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; +import { + ManageDashboardCustomersDialogComponent +} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; import { DashboardRoutingModule } from './dashboard-routing.module'; -import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; +import { + MakeDashboardPublicDialogComponent +} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; -import {ImportDashboardFileDialogComponent} from "@home/pages/dashboard/import-dashboard-file-dialog.component"; +import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component"; @NgModule({ declarations: [ @@ -42,4 +46,5 @@ import {ImportDashboardFileDialogComponent} from "@home/pages/dashboard/import-d DashboardRoutingModule ] }) -export class DashboardModule { } +export class DashboardModule { +} From 50794bdbc88a5ce3a0d6129eb102c4bc89891e8f Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 17:05:05 +0300 Subject: [PATCH 013/121] Refactored formatting --- .../pages/dashboard/import-dashboard-file-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts index 26238655a0..01fca78bf9 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -45,7 +45,7 @@ export class ImportDashboardFileDialogComponent extends DialogComponent, - public fb: FormBuilder) { + private fb: FormBuilder) { super(store, router, dialogRef); this.dashboard = data.dashboard; } From 7bc16bcbb9dd3d96a221c347cb13ef957bb9b719 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Tue, 9 Sep 2025 18:42:09 +0300 Subject: [PATCH 014/121] Map widget: fixed lat/long keys duplication --- .../src/app/shared/models/widget/maps/map-model.definition.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts index 8013889017..1be4ecd5c6 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts @@ -31,6 +31,7 @@ import { PolygonsDataLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { deepClone } from '@core/utils'; interface AliasFilterPair { alias?: EntityAliasInfo, @@ -271,7 +272,7 @@ const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[], const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, dataLayerType: MapDataLayerType): DataKey[] => { - const dataKeys = settings.additionalDataKeys || []; + const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; switch (dataLayerType) { case 'markers': const markersSettings = settings as MarkersDataLayerSettings; From e05583cd2bb66950e9c03981a63b558b585d731d Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 10 Sep 2025 13:14:35 +0300 Subject: [PATCH 015/121] Changed text for translation --- .../modules/home/pages/dashboard/dashboard-form.component.html | 2 +- .../pages/dashboard/import-dashboard-file-dialog.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 2e185d66ad..755fb45db1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -32,7 +32,7 @@ [disabled]="(isLoading$ | async)" (click)="onEntityAction($event, 'import')" [class.!hidden]="isEdit || dashboardScope !== 'tenant'"> - {{'dashboard.import' | translate }} + {{'dashboard.update-new-version' | translate }}