From e08f627ae125f5e6ad0ff5f4dcc8b4066552d4d1 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Thu, 14 Aug 2025 16:09:41 +0300 Subject: [PATCH 001/147] Improve Edge stats reporting and add related integration tests --- .../service/edge/rpc/EdgeGrpcSession.java | 2 +- .../service/edge/stats/EdgeStatsService.java | 18 +- .../server/edge/EdgeStatsIntegrationTest.java | 293 ++++++++++++++++++ .../server/service/edge/EdgeStatsTest.java | 68 ++-- .../thingsboard/edge/rpc/EdgeRpcClient.java | 1 + common/edge-api/src/main/proto/edge.proto | 1 + .../server/dao/edge/stats/EdgeStats.java | 34 ++ .../edge/stats/EdgeStatsCounterService.java | 16 +- 8 files changed, 381 insertions(+), 52 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 521730741f..c9364accd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -301,7 +301,7 @@ public abstract class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { if (fetcher instanceof GeneralEdgeEventFetcher) { long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize)); } log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 48b2a47cfb..697f7b6216 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.edge.stats.EdgeStats; import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -80,13 +81,14 @@ public class EdgeStatsService { long now = System.currentTimeMillis(); long ts = now - (now % reportIntervalMillis); - Map countersByEdge = statsCounterService.getCounterByEdge(); - Map lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); - Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge()); - countersByEdgeSnapshot.forEach((edgeId, counters) -> { + Map statsByEdgeSnapshot = new HashMap<>(statsCounterService.getStatsByEdge()); + boolean isKafkaStats = kafkaAdmin.isPresent(); + Map lagByEdgeId = isKafkaStats ? getLagByEdgeId(statsByEdgeSnapshot) : Collections.emptyMap(); + statsByEdgeSnapshot.forEach((edgeId, edgeStats) -> { + MsgCounters counters = edgeStats.getMsgCounters(); TenantId tenantId = counters.getTenantId(); - if (kafkaAdmin.isPresent()) { + if (isKafkaStats) { counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L)); } List statsEntries = List.of( @@ -102,11 +104,11 @@ public class EdgeStatsService { }); } - private Map getEdgeLagByEdgeId(Map countersByEdge) { - Map edgeToTopicMap = countersByEdge.entrySet().stream() + private Map getLagByEdgeId(Map edgeStatsByEdge) { + Map edgeToTopicMap = edgeStatsByEdge.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, - e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getTenantId(), e.getKey()).getTopic() + e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getMsgCounters().getTenantId(), e.getKey()).getTopic() )); Map lagByTopic = kafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values())); diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java new file mode 100644 index 0000000000..b993e4f564 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -0,0 +1,293 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.AbstractMessage; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.dao.edge.stats.MsgCounters; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.edge.stats.EdgeStatsService; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; + +@DaoSqlTest +@Slf4j +public class EdgeStatsIntegrationTest extends AbstractEdgeTest { + + private static final String STATISTICS_DEVICE_PROFILE = "STATISTICS"; + + private final Map EXPECTED_EDGE_STATS = Map.of( + DOWNLINK_MSGS_ADDED, 6L, + DOWNLINK_MSGS_PUSHED, 6L, + DOWNLINK_MSGS_PERMANENTLY_FAILED, 0L, + DOWNLINK_MSGS_TMP_FAILED, 0L + ); + + private final Map EXPECTED_EMPTY_EDGE_STATS = Map.of( + DOWNLINK_MSGS_ADDED, 0L, + DOWNLINK_MSGS_PUSHED, 0L, + DOWNLINK_MSGS_PERMANENTLY_FAILED, 0L, + DOWNLINK_MSGS_TMP_FAILED, 0L + ); + + @Autowired + private EdgeStatsService edgeStatsService; + @Autowired + private EdgeStatsCounterService statsCounterService; + + @Test + public void testFullEdgeStatsCycle() throws Exception { + // 1. Clear previous stats and prepare test data + prepareTestData(); + + // 2. Wait until Edge counters are updated + awaitEdgeCountersUpdated(); + + // 3. Report statistics + edgeStatsService.reportStats(); + + // 4. Wait until timeseries data is persisted + awaitStatsMatch(EXPECTED_EDGE_STATS); + + // 5. Send statistics to Edge + List latestStatsEntries = tsService.findLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() + ).get(); + sendStatsToEdge(latestStatsEntries); + + // 6. Wait until telemetry Proto contains the expected stats + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + EntityDataProto latestMsg = getLatestEntityDataMessage(); + Map actualStats = toMap(latestMsg.getPostTelemetryMsg().getTsKvList(0)); + assertAllStatsEqual(EXPECTED_EDGE_STATS, actualStats, "Proto stats"); + }); + } + + @Test + public void testNoMessagesFromEdge() throws ExecutionException, InterruptedException { + // 1. Clear stats counters for the Edge + statsCounterService.clear(edge.getId()); + + // 2. Report stats with no data from the Edge + edgeStatsService.reportStats(); + + // 3. Verify that persisted timeseries contains only empty stats + awaitStatsMatch(EXPECTED_EMPTY_EDGE_STATS); + + List actual = tsService.findLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() + ).get(); + + assertAllStatsEqual(EXPECTED_EMPTY_EDGE_STATS, toMap(actual), "Empty stats"); + } + + @Test + public void testRepeatedReportStatsDoesNotDuplicate() throws ExecutionException, InterruptedException { + // 1. Clear previous stats and prepare test data + prepareTestData(); + + // 2. Wait until Edge counters are updated + awaitEdgeCountersUpdated(); + + // 3. First report call + edgeStatsService.reportStats(); + + // 4. Verify that the persisted stats match expectations + awaitStatsMatch(EXPECTED_EDGE_STATS); + + // 5. Remove persisted stats to simulate a re-report scenario + tsService.removeLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() + ); + + // 6. Second report call without increments (counters already cleared) + edgeStatsService.reportStats(); + + // 7. Verify that the stats are empty after the second report + awaitStatsMatch(EXPECTED_EMPTY_EDGE_STATS); + } + + private void awaitStatsMatch(Map expected) { + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + Map actualStats = fetchLatestStats(); + assertAllStatsEqual(expected, actualStats, "Timeseries stats"); + }); + } + + private Map fetchLatestStats() throws ExecutionException, InterruptedException { + List latestStatsEntries = tsService.findLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() + ).get(); + return toMap(latestStatsEntries); + } + + private void prepareTestData() throws InterruptedException, ExecutionException { + statsCounterService.clear(edge.getId()); + // 2 stats message ADDED Device Profile, ASSIGN Device + edgeImitator.expectMessageAmount(4); + Device device = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); + doPost("/api/edge/" + edge.getUuidId() + "/device/" + device.getUuidId(), Device.class); + edgeImitator.waitForMessages(); + // 1 stats message ASSIGN Asset + edgeImitator.expectMessageAmount(2); + Asset savedAsset = saveAsset("Edge Asset 2"); + doPost("/api/edge/" + edge.getUuidId() + + "/asset/" + savedAsset.getUuidId(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // 2 stats message ADDED Customer, ASSIGN Customer + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle("Edge Customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + // assign edge to customer + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + //1 stats message Timeseries + edgeImitator.expectMessageAmount(1); + String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(edgeEvent).get(); + Assert.assertTrue(edgeImitator.waitForMessages()); + } + + private void assertAllStatsEqual(Map expected, Map actual, String context) { + assertAll(context, + expected.entrySet().stream() + .map(e -> () -> assertEquals(e.getValue(), actual.get(e.getKey().getKey()), "Mismatch for stat: " + e.getKey())) + ); + } + + private void awaitEdgeCountersUpdated() { + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + MsgCounters counters = statsCounterService.getStatsByEdge().get(edge.getId()).getMsgCounters(); + Map actualCounters = toMap( + Map.entry(DOWNLINK_MSGS_ADDED.getKey(), () -> counters.getMsgsAdded().get()), + Map.entry(DOWNLINK_MSGS_PUSHED.getKey(), () -> counters.getMsgsPushed().get()), + Map.entry(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey(), () -> counters.getMsgsPermanentlyFailed().get()), + Map.entry(DOWNLINK_MSGS_TMP_FAILED.getKey(), () -> counters.getMsgsTmpFailed().get()) + ); + assertAllStatsEqual(EXPECTED_EDGE_STATS, actualCounters, "Edge counters"); + }); + } + + @SafeVarargs + private final Map toMap(Map.Entry>... suppliers) { + return Arrays.stream(suppliers) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + } + + private Map toMap(List stats) { + return stats.stream() + .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(0L))); + } + + private Map toMap(TransportProtos.TsKvListProto kvList) { + Map map = kvList.getKvList().stream() + .collect(Collectors.toMap( + TransportProtos.KeyValueProto::getKey, + TransportProtos.KeyValueProto::getLongV + )); + for (EdgeStatsKey key : EdgeStatsKey.values()) { + map.putIfAbsent(key.getKey(), 0L); + } + return map; + } + + private void sendStatsToEdge(List stats) throws Exception { + edgeImitator.expectMessageAmount(1); + EdgeEvent edgeEvent = constructEdgeEvent( + tenantId, + edge.getId(), + EdgeEventActionType.TIMESERIES_UPDATED, + edge.getId().getId(), + EdgeEventType.EDGE, + buildStatsJson(System.currentTimeMillis(), stats) + ); + edgeEventService.saveAsync(edgeEvent).get(); + assertTrue(edgeImitator.waitForMessages()); + } + + private EntityDataProto getLatestEntityDataMessage() { + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + assertInstanceOf(EntityDataProto.class, latestMessage); + EntityDataProto msg = (EntityDataProto) latestMessage; + assertEquals(edge.getUuidId().getMostSignificantBits(), msg.getEntityIdMSB()); + assertEquals(edge.getUuidId().getLeastSignificantBits(), msg.getEntityIdLSB()); + assertEquals(edge.getId().getEntityType().name(), msg.getEntityType()); + assertTrue(msg.hasPostTelemetryMsg()); + return msg; + } + + private ObjectNode buildStatsJson(long ts, List statsEntries) { + ObjectNode entityBody = JacksonUtil.newObjectNode(); + entityBody.put("ts", ts); + ObjectNode data = JacksonUtil.newObjectNode(); + statsEntries.forEach(entry -> data.put(entry.getKey(), entry.getValueAsString())); + entityBody.set("data", data); + return entityBody; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index 25ff0f1b5d..fb24cdb5bb 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.edge.stats.EdgeStats; import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -44,9 +45,11 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; @@ -58,6 +61,9 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_T @ExtendWith(MockitoExtension.class) public class EdgeStatsTest { + private static final int TTL_DAYS = 30; + private static final long REPORT_INTERVAL_MILLIS = 600_000L; + @Mock private TimeseriesService tsService; @Mock @@ -71,40 +77,45 @@ public class EdgeStatsTest { @BeforeEach void setUp() { - edgeStatsService = new EdgeStatsService( + edgeStatsService = createEdgeStatsService(Optional.empty()); + } + + private EdgeStatsService createEdgeStatsService(Optional kafkaAdmin) { + EdgeStatsService service = new EdgeStatsService( tsService, statsCounterService, topicService, - Optional.empty() + kafkaAdmin ); - - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + ReflectionTestUtils.setField(service, "edgesStatsTtlDays", TTL_DAYS); + ReflectionTestUtils.setField(service, "reportIntervalMillis", REPORT_INTERVAL_MILLIS); + return service; } @Test public void testReportStatsSavesTelemetry() { - // given - MsgCounters counters = new MsgCounters(tenantId); + EdgeStats edgeStats = new EdgeStats(tenantId); + MsgCounters counters = edgeStats.getMsgCounters(); counters.getMsgsAdded().set(5); counters.getMsgsPushed().set(3); counters.getMsgsPermanentlyFailed().set(1); counters.getMsgsTmpFailed().set(0); counters.getMsgsLag().set(10); - ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); - countersByEdge.put(edgeId, counters); + ConcurrentHashMap edgeStatsByEdge = new ConcurrentHashMap<>(); + edgeStatsByEdge.put(edgeId, edgeStats); - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + when(statsCounterService.getStatsByEdge()).thenReturn(edgeStatsByEdge); - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> captor = ArgumentCaptor.forClass((Class) List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); - // when edgeStatsService.reportStats(); - // then + verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); + verify(statsCounterService, times(1)).clear(edgeId); + List entries = captor.getValue(); Assertions.assertEquals(5, entries.size()); @@ -116,26 +127,22 @@ public class EdgeStatsTest { Assertions.assertEquals(1L, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); Assertions.assertEquals(0L, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); Assertions.assertEquals(10L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); - - - verify(statsCounterService).clear(edgeId); } @Test public void testReportStatsWithKafkaLag() { - // given - MsgCounters counters = new MsgCounters(tenantId); + EdgeStats edgeStats = new EdgeStats(tenantId); + MsgCounters counters = edgeStats.getMsgCounters(); counters.getMsgsAdded().set(2); counters.getMsgsPushed().set(2); counters.getMsgsPermanentlyFailed().set(0); counters.getMsgsTmpFailed().set(1); counters.getMsgsLag().set(0); - ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); - countersByEdge.put(edgeId, counters); + ConcurrentHashMap edgeStatsByEdge = new ConcurrentHashMap<>(); + edgeStatsByEdge.put(edgeId, edgeStats); - // mocks - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + when(statsCounterService.getStatsByEdge()).thenReturn(edgeStatsByEdge); String topic = "edge-topic"; TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); @@ -145,29 +152,22 @@ public class EdgeStatsTest { when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) .thenReturn(Map.of(topic, 15L)); - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> captor = ArgumentCaptor.forClass((Class) List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); - edgeStatsService = new EdgeStatsService( - tsService, - statsCounterService, - topicService, - Optional.of(kafkaAdmin) - ); - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + edgeStatsService = createEdgeStatsService(Optional.of(kafkaAdmin)); - // when edgeStatsService.reportStats(); - // then + verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); + verify(statsCounterService, times(1)).clear(edgeId); + List entries = captor.getValue(); Map valuesByKey = entries.stream() .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); Assertions.assertEquals(15L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); - verify(statsCounterService).clear(edgeId); } } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index 423c59251b..ed0d5b603e 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -41,4 +41,5 @@ public interface EdgeRpcClient { void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); int getServerMaxInboundMessageSize(); + } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index dbda462a99..aea505c2fe 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -44,6 +44,7 @@ enum EdgeVersion { V_4_0_0 = 10; V_4_1_0 = 11; V_4_2_0 = 12; + V_4_3_0 = 13; V_LATEST = 999; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java new file mode 100644 index 0000000000..6f34563f96 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.edge.stats; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Data +public class EdgeStats { + private final MsgCounters msgCounters; + private final Queue uplinkRate; + + public EdgeStats(TenantId tenantId) { + this.msgCounters = new MsgCounters(tenantId); + this.uplinkRate = new ConcurrentLinkedQueue<>(); + } + +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java index 16111cf514..c537fb75be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java @@ -30,28 +30,26 @@ import java.util.concurrent.ConcurrentHashMap; @Getter public class EdgeStatsCounterService { - private final ConcurrentHashMap counterByEdge = new ConcurrentHashMap<>(); + private final ConcurrentHashMap statsByEdge = new ConcurrentHashMap<>(); public void recordEvent(EdgeStatsKey type, TenantId tenantId, EdgeId edgeId, long value) { - MsgCounters counters = getOrCreateCounters(tenantId, edgeId); + EdgeStats edgeStats = getOrCreateEdgeStats(tenantId, edgeId); + MsgCounters counters = edgeStats.getMsgCounters(); switch (type) { case DOWNLINK_MSGS_ADDED -> counters.getMsgsAdded().addAndGet(value); case DOWNLINK_MSGS_PUSHED -> counters.getMsgsPushed().addAndGet(value); case DOWNLINK_MSGS_PERMANENTLY_FAILED -> counters.getMsgsPermanentlyFailed().addAndGet(value); case DOWNLINK_MSGS_TMP_FAILED -> counters.getMsgsTmpFailed().addAndGet(value); + case DOWNLINK_MSGS_LAG -> counters.getMsgsLag().set(value); } } - public void setDownlinkMsgsLag(TenantId tenantId, EdgeId edgeId, long value) { - getOrCreateCounters(tenantId, edgeId).getMsgsLag().set(value); + public EdgeStats getOrCreateEdgeStats(TenantId tenantId, EdgeId edgeId) { + return statsByEdge.computeIfAbsent(edgeId, id -> new EdgeStats(tenantId)); } public void clear(EdgeId edgeId) { - counterByEdge.remove(edgeId); - } - - public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { - return counterByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); + statsByEdge.remove(edgeId); } } From d35be826b8f68239da443c116c30bdba5732d497 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Thu, 14 Aug 2025 16:13:10 +0300 Subject: [PATCH 002/147] Refactor --- .../java/org/thingsboard/server/dao/edge/stats/EdgeStats.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java index 6f34563f96..36d87691cc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java @@ -31,4 +31,4 @@ public class EdgeStats { this.uplinkRate = new ConcurrentLinkedQueue<>(); } -} \ No newline at end of file +} From aff9b5ac25fe7be3b064bd89bf0e0e883a0821ab Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Nov 2025 16:57:08 +0200 Subject: [PATCH 003/147] new endpoints --- .../controller/AssetProfileController.java | 34 +++++++++++++++ .../server/controller/CustomerController.java | 35 +++++++++++++++ .../controller/DashboardController.java | 40 +++++++++++++---- .../controller/DeviceProfileController.java | 33 ++++++++++++++ .../controller/RuleChainController.java | 31 +++++++++++++ .../server/controller/TenantController.java | 32 ++++++++++++++ .../controller/TenantProfileController.java | 1 - .../server/controller/UserController.java | 30 +++++++++++++ .../controller/WidgetsBundleController.java | 38 ++++++++++++++++ .../permission/AccessControlService.java | 2 + .../DefaultAccessControlService.java | 10 +++++ .../permission/PermissionChecker.java | 1 + .../AssetProfileControllerTest.java | 36 ++++++++++++++++ .../controller/CustomerControllerTest.java | 39 +++++++++++++++++ .../controller/DashboardControllerTest.java | 39 +++++++++++++++++ .../DeviceProfileControllerTest.java | 40 +++++++++++++++++ .../controller/RuleChainControllerTest.java | 43 +++++++++++++++++++ .../controller/TenantControllerTest.java | 38 ++++++++++++++++ .../TenantProfileControllerTest.java | 41 ++++++++++++++++++ .../server/controller/UserControllerTest.java | 35 +++++++++++++++ .../WidgetsBundleControllerTest.java | 36 ++++++++++++++++ .../server/dao/asset/AssetProfileService.java | 3 ++ .../server/dao/customer/CustomerService.java | 3 ++ .../dao/dashboard/DashboardService.java | 2 + .../dao/device/DeviceProfileService.java | 3 ++ .../server/dao/rule/RuleChainService.java | 2 + .../server/dao/tenant/TenantService.java | 2 + .../server/dao/user/UserService.java | 3 ++ .../dao/widget/WidgetsBundleService.java | 5 +++ .../server/common/data/DeviceProfileInfo.java | 2 +- .../common/data/asset/AssetProfileInfo.java | 3 +- .../server/dao/asset/AssetProfileDao.java | 3 ++ .../dao/asset/AssetProfileServiceImpl.java | 11 +++++ .../server/dao/customer/CustomerDao.java | 4 ++ .../dao/customer/CustomerServiceImpl.java | 10 +++++ .../dao/dashboard/DashboardInfoDao.java | 4 ++ .../dao/dashboard/DashboardServiceImpl.java | 9 ++++ .../server/dao/device/DeviceProfileDao.java | 3 ++ .../dao/device/DeviceProfileServiceImpl.java | 11 +++++ .../server/dao/rule/BaseRuleChainService.java | 9 ++++ .../server/dao/rule/RuleChainDao.java | 4 ++ .../dao/sql/asset/AssetProfileRepository.java | 6 +++ .../dao/sql/asset/JpaAssetProfileDao.java | 6 +++ .../dao/sql/customer/CustomerRepository.java | 2 + .../dao/sql/customer/JpaCustomerDao.java | 6 +++ .../dashboard/DashboardInfoRepository.java | 3 ++ .../sql/dashboard/JpaDashboardInfoDao.java | 6 +++ .../sql/device/DeviceProfileRepository.java | 5 +++ .../dao/sql/device/JpaDeviceProfileDao.java | 6 +++ .../server/dao/sql/rule/JpaRuleChainDao.java | 6 +++ .../dao/sql/rule/RuleChainRepository.java | 3 ++ .../server/dao/sql/tenant/JpaTenantDao.java | 6 +++ .../dao/sql/tenant/TenantRepository.java | 2 + .../server/dao/sql/user/JpaUserDao.java | 6 +++ .../server/dao/sql/user/UserRepository.java | 3 ++ .../dao/sql/widget/JpaWidgetsBundleDao.java | 12 ++++++ .../sql/widget/WidgetsBundleRepository.java | 12 ++++++ .../server/dao/tenant/TenantDao.java | 3 ++ .../server/dao/tenant/TenantServiceImpl.java | 9 ++++ .../thingsboard/server/dao/user/UserDao.java | 4 ++ .../server/dao/user/UserServiceImpl.java | 10 +++++ .../server/dao/widget/WidgetsBundleDao.java | 6 +++ .../dao/widget/WidgetsBundleServiceImpl.java | 20 +++++++++ 63 files changed, 861 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 96b846eefa..9415e7194e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -16,11 +16,13 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,7 +47,10 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; @@ -222,4 +227,33 @@ public class AssetProfileController extends BaseController { return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly)); } + @ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)", + notes = "Requested asset profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"}) + public List getAssetProfilesByIds( + @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("assetProfileIds") String[] strAssetProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("assetProfileIds", strAssetProfileIds); + + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List assetProfileIds = new ArrayList<>(); + for (String strAssetProfileId : strAssetProfileIds) { + assetProfileIds.add(new AssetProfileId(toUUID(strAssetProfileId))); + } + + return Objects.requireNonNull(checkNotNull(assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.ASSET_PROFILE, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 03ac9c60fa..e0f8cef1c8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -18,10 +18,12 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,9 +42,15 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.customer.TbCustomerService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; @@ -183,4 +191,31 @@ public class CustomerController extends BaseController { return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); } + @ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", + notes = "Returns a list of Customer objects based on the provided ids." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/customers", params = {"customerIds"}) + public List getCustomersByIds( + @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("customerIds") String[] strCustomerIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("customerIds", strCustomerIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List customerIds = new ArrayList<>(); + for (String strCustomerId : strCustomerIds) { + customerIds.add(new CustomerId(toUUID(strCustomerId))); + } + return Objects.requireNonNull(checkNotNull(customerService.findCustomersByTenantIdAndIdsAsync(tenantId, customerIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.CUSTOMER, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 0e3fde0031..f23c3d3e19 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -64,10 +65,12 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -604,14 +607,7 @@ public class DashboardController extends BaseController { checkEdgeId(edgeId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); - List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + List filteredDashboards = filterDashboardsByReadPermission(nonFilteredResult.getData()); PageData filteredResult = new PageData<>(filteredDashboards, nonFilteredResult.getTotalPages(), nonFilteredResult.getTotalElements(), @@ -619,6 +615,24 @@ public class DashboardController extends BaseController { return checkNotNull(filteredResult); } + @ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)", + notes = "Returns a list of DashboardInfo objects based on the provided ids. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/dashboards", params = {"dashboardIds"}) + public List getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("dashboardIds") String[] strDashboardIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("dashboardIds", strDashboardIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List dashboardIds = new ArrayList<>(); + for (String strDashboardId : strDashboardIds) { + dashboardIds.add(new DashboardId(toUUID(strDashboardId))); + } + List dashboards = checkNotNull(dashboardService.findDashboardInfoByIdsAsync(tenantId, dashboardIds).get()); + return filterDashboardsByReadPermission(dashboards); + } + private Set customerIdFromStr(String[] strCustomerIds) { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { @@ -629,4 +643,14 @@ public class DashboardController extends BaseController { return customerIds; } + private List filterDashboardsByReadPermission(List dashboards) { + return dashboards.stream().filter(dashboard -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboard.getId(), dashboard); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 07f25cd14d..7f6f6bfa61 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -16,12 +16,14 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -48,8 +50,11 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; +import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID; @@ -278,4 +283,32 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); } + @ApiOperation(value = "Get Device Profiles By Ids (getDeviceProfilesByIds)", + notes = "Requested device profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"}) + public List getDeviceProfilesByIds( + @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("deviceProfileIds") String[] strDeviceProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("deviceProfileIds", strDeviceProfileIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List deviceProfileIds = new ArrayList<>(); + for (String strDeviceProfileId : strDeviceProfileIds) { + deviceProfileIds.add(new DeviceProfileId(toUUID(strDeviceProfileId))); + } + + return Objects.requireNonNull(checkNotNull(deviceProfileService.findDeviceProfilesByIdsAsync(tenantId, deviceProfileIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.DEVICE_PROFILE, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index efe3e61893..4349ea22bf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -71,12 +72,14 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentMap; @@ -90,6 +93,7 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIG import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -579,4 +583,31 @@ public class RuleChainController extends BaseController { return checkNotNull(result); } + @ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)", + notes = "Requested rule chains must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/ruleChains", params = {"ruleChainIds"}) + public List getRuleChainsByIds( + @Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ruleChainIds") String[] strRuleChainIds) throws Exception { + checkArrayParameter("ruleChainIds", strRuleChainIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List ruleChainIds = new ArrayList<>(); + for (String strRuleChainId : strRuleChainIds) { + ruleChainIds.add(new RuleChainId(toUUID(strRuleChainId))); + } + return Objects.requireNonNull(checkNotNull(ruleChainService.findRuleChainsByIdsAsync(tenantId, ruleChainIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.RULE_CHAIN, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 3ded7b7171..22ceb33b8d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -16,11 +16,13 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,9 +41,15 @@ import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.tenant.TbTenantService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -166,4 +174,28 @@ public class TenantController extends BaseController { return checkNotNull(tenantService.findTenantInfos(pageLink)); } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/tenants", params = {"tenantIds"}) + public List getTenantsByIds( + @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("tenantIds") String[] strTenantIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("tenantIds", strTenantIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List tenantIds = new ArrayList<>(); + for (String strTenantId : strTenantIds) { + tenantIds.add(new TenantId(toUUID(strTenantId))); + } + return Objects.requireNonNull(checkNotNull(tenantService.findTenantsByIdsAsync(tenantId, tenantIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.TENANT, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 1a3bdce1f6..71c8e1e4bc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -264,5 +264,4 @@ public class TenantProfileController extends BaseController { return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); } - } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index a2a7c993e9..e0adf27a97 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -84,6 +85,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; @@ -593,6 +597,32 @@ public class UserController extends BaseController { userService.removeMobileSession(user.getTenantId(), mobileToken); } + @ApiOperation(value = "Get Users By Ids (getUsersByIds)", + notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/users", params = {"userIds"}) + public List getUsersByIds( + @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("userIds") String[] strUserIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("userIds", strUserIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List userIds = new ArrayList<>(); + for (String strUserId : strUserIds) { + userIds.add(new UserId(toUUID(strUserId))); + } + return Objects.requireNonNull(checkNotNull(userService.findUsersByTenantIdAndIdsAsync(tenantId, userIds).get())) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(user, Resource.USER, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { if (type.isReserved()) { throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 7c3133d6d9..1060c7fe94 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -16,10 +16,12 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,13 +47,17 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -234,4 +240,36 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesByIds)", + notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"}) + public List getWidgetsBundlesByIds( + @Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("widgetsBundleIds") String[] strWidgetsBundleIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("widgetsBundleIds", strWidgetsBundleIds); + List widgetsBundleIds = new ArrayList<>(); + for (String strWidgetsBundleId : strWidgetsBundleIds) { + widgetsBundleIds.add(new WidgetsBundleId(toUUID(strWidgetsBundleId))); + } + List result; + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + result = checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get()); + } else { + result = checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get()); + } + + return Objects.requireNonNull(result) + .stream() + .filter(e -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, Operation.READ, e.getId(), e); + } catch (ThingsboardException ex) { + return false; + } + }) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java index 9f693877cd..b760a54ade 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java @@ -26,4 +26,6 @@ public interface AccessControlService { void checkPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; + boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index a5feb1c502..9bf05e2d73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -65,6 +65,16 @@ public class DefaultAccessControlService implements AccessControlService { } } + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException { + PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); + if (permissionChecker != null) { + return permissionChecker.hasPermission(user, operation, entityId, entity); + } + return false; + } + private PermissionChecker getPermissionChecker(Authority authority, Resource resource) throws ThingsboardException { Permissions permissions = authorityPermissions.get(authority); if (permissions == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java index 2fcf6b71fc..216d74fe2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.permission; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java index 2691352d01..14ac8c91d1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java @@ -46,9 +46,13 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; @@ -195,6 +199,38 @@ public class AssetProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName()); } + @Test + public void testFindAssetProfileInfoByIds() throws Exception { + List assetProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i); + assetProfile.setTenantId(savedTenant.getId()); + assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfileInfo.class)); + } + + List expected = assetProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(ap -> ap.getId().getId().toString()) + .collect(Collectors.joining(",")); + AssetProfileInfo[] foundAssetProfileInfos = doGet("/api/assetProfileInfos?assetProfileIds=" + idsParam, AssetProfileInfo[].class); + + Assert.assertNotNull(foundAssetProfileInfos); + Assert.assertEquals(expected.size(), foundAssetProfileInfos.length); + + Map infoById = Arrays.stream(foundAssetProfileInfos) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (AssetProfileInfo assetProfileInfo : expected) { + UUID id = assetProfileInfo.getId().getId(); + AssetProfileInfo info = infoById.get(id); + Assert.assertNotNull("AssetProfileInfo not found for id " + id, info); + + Assert.assertEquals(assetProfileInfo.getId(), info.getId()); + Assert.assertEquals(assetProfileInfo.getName(), info.getName()); + } + } + @Test public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception { AssetProfile assetProfile = createAssetProfile("Asset profile 1"); diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index f312e49f71..6e97e1cb99 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -47,8 +47,13 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -260,6 +265,40 @@ public class CustomerControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testFindCustomersByIds() throws Exception { + List savedCustomers = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Customer customer = new Customer(); + customer.setTitle("My customer " + i); + savedCustomers.add(doPost("/api/customer", customer, Customer.class)); + } + + String idsParam = savedCustomers.stream() + .map(c -> c.getId().getId().toString()) + .collect(Collectors.joining(",")); + + Customer[] foundCustomers = doGet("/api/customers?customerIds=" + idsParam, Customer[].class); + + Assert.assertNotNull(foundCustomers); + Assert.assertEquals(savedCustomers.size(), foundCustomers.length); + + Map foundById = Arrays.stream(foundCustomers) + .collect(Collectors.toMap(c -> c.getId().getId(), Function.identity())); + + for (Customer savedCustomer : savedCustomers) { + UUID id = savedCustomer.getId().getId(); + Customer foundCustomer = foundById.get(id); + Assert.assertNotNull("Customer not found for id " + id, foundCustomer); + Assert.assertEquals(savedCustomer, foundCustomer); + } + + for (Customer savedCustomer : savedCustomers) { + doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + @Test public void testDeleteCustomer() throws Exception { Customer customer = new Customer(); diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index a73bc3befb..052ff8777b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -55,9 +55,12 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -155,6 +158,42 @@ public class DashboardControllerTest extends AbstractControllerTest { Assert.assertEquals(savedDashboard, foundDashboard); } + @Test + public void testFindDashboardInfosByIds() throws Exception { + List dashboards = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard " + i); + dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class)); + } + + List expected = dashboards.subList(5, 15); + + String idsParam = expected.stream() + .map(d -> d.getId().getId().toString()) + .collect(Collectors.joining(",")); + + DashboardInfo[] result = doGet( + "/api/dashboards?dashboardIds=" + idsParam, + DashboardInfo[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map infoById = Arrays.stream(result) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (Dashboard dashboard : expected) { + UUID id = dashboard.getId().getId(); + DashboardInfo info = infoById.get(id); + Assert.assertNotNull("DashboardInfo not found for id " + id, info); + + Assert.assertEquals(dashboard.getId(), info.getId()); + Assert.assertEquals(dashboard.getTitle(), info.getTitle()); + } + } + @Test public void testDeleteDashboard() throws Exception { Dashboard dashboard = new Dashboard(); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index 320e3815f6..fb0e3fb57a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -56,9 +56,13 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -214,6 +218,42 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType()); } + @Test + public void testFindDeviceProfileInfosByIds() throws Exception { + List deviceProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile " + i); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); + } + + List expected = deviceProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(dp -> dp.getId().getId().toString()) + .collect(Collectors.joining(",")); + + DeviceProfileInfo[] result = doGet( + "/api/deviceProfileInfos?deviceProfileIds=" + idsParam, + DeviceProfileInfo[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map infoById = Arrays.stream(result) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (DeviceProfile dp : expected) { + UUID id = dp.getId().getId(); + DeviceProfileInfo info = infoById.get(id); + Assert.assertNotNull("DeviceProfileInfo not found for id " + id, info); + + Assert.assertEquals(dp.getId(), info.getId()); + Assert.assertEquals(dp.getName(), info.getName()); + Assert.assertEquals(dp.getType(), info.getType()); + } + } + @Test public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index a6a022347f..72a6697927 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -52,7 +52,12 @@ import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -231,6 +236,44 @@ public class RuleChainControllerTest extends AbstractControllerTest { Assert.assertEquals(savedRuleChain, foundRuleChain); } + @Test + public void testFindRuleChainsByIds() throws Exception { + List ruleChains = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("RuleChain " + i); + ruleChains.add(doPost("/api/ruleChain", ruleChain, RuleChain.class)); + } + + List expected = ruleChains.subList(5, 15); + + String idsParam = expected.stream() + .map(rc -> rc.getId().getId().toString()) + .collect(Collectors.joining(",")); + + RuleChain[] result = doGet( + "/api/ruleChains?ruleChainIds=" + idsParam, + RuleChain[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map rcById = Arrays.stream(result) + .collect(Collectors.toMap(rc -> rc.getId().getId(), Function.identity())); + + for (RuleChain rc : expected) { + UUID id = rc.getId().getId(); + RuleChain found = rcById.get(id); + Assert.assertNotNull("RuleChain not found for id " + id, found); + + Assert.assertEquals(rc.getId(), found.getId()); + Assert.assertEquals(rc.getName(), found.getName()); + Assert.assertEquals(rc.getTenantId(), found.getTenantId()); + } + } + + @Test public void testDeleteRuleChain() throws Exception { RuleChain ruleChain = new RuleChain(); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index ba45be8e28..cfab873ea1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -68,6 +68,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -77,6 +78,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -187,6 +189,42 @@ public class TenantControllerTest extends AbstractControllerTest { deleteTenant(savedTenant.getId()); } + @Test + public void testFindTenantsByIds() throws Exception { + loginSysAdmin(); + + List savedTenants = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + i); + savedTenants.add(saveTenant(tenant)); + } + + String idsParam = savedTenants.stream() + .map(t -> t.getId().getId().toString()) + .collect(Collectors.joining(",")); + + Tenant[] foundTenants = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class); + + Assert.assertNotNull(foundTenants); + Assert.assertEquals(savedTenants.size(), foundTenants.length); + + Map foundById = Arrays.stream(foundTenants) + .collect(Collectors.toMap(t -> t.getId().getId(), Function.identity())); + + for (Tenant savedTenant : savedTenants) { + UUID id = savedTenant.getId().getId(); + Tenant foundTenant = foundById.get(id); + Assert.assertNotNull("Tenant not found for id " + id, foundTenant); + Assert.assertEquals(savedTenant, foundTenant); + } + + for (Tenant savedTenant : savedTenants) { + deleteTenant(savedTenant.getId()); + } + } + + @Test public void testFindTenantInfoById() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java index 8f83f2c186..4862a00125 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java @@ -42,8 +42,12 @@ import org.thingsboard.server.queue.TbQueueCallback; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; @@ -120,6 +124,43 @@ public class TenantProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName()); } + @Test + public void testFindTenantProfilesByIds() throws Exception { + loginSysAdmin(); + List tenantProfiles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile " + i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + List expected = tenantProfiles.subList(2, 7); + + String idsParam = expected.stream() + .map(tp -> tp.getId().getId().toString()) + .collect(Collectors.joining(",")); + + TenantProfile[] result = doGet( + "/api/tenantProfiles?ids=" + idsParam, + TenantProfile[].class + ); + + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.length); + + Map tpById = Arrays.stream(result) + .collect(Collectors.toMap(tp -> tp.getId().getId(), Function.identity())); + + for (TenantProfile tp : expected) { + UUID id = tp.getId().getId(); + TenantProfile found = tpById.get(id); + Assert.assertNotNull("TenantProfile not found for id " + id, found); + + Assert.assertEquals(tp.getId(), found.getId()); + Assert.assertEquals(tp.getName(), found.getName()); + } + } + + @Test public void testFindDefaultTenantProfileInfo() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 28d43699fd..4379b57b45 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -55,8 +55,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserDao; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -263,6 +267,37 @@ public class UserControllerTest extends AbstractControllerTest { Assert.assertEquals(savedUser, foundUser); } + @Test + public void testFindUsersByIds() throws Exception { + loginSysAdmin(); + List savedUsers = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + User user = createTenantAdminUser(); + savedUsers.add(doPost("/api/user", user, User.class)); + } + + String idsParam = savedUsers.stream() + .map(u -> u.getId().getId().toString()) + .collect(Collectors.joining(",")); + + User[] foundUsers = doGet("/api/users?userIds=" + idsParam, User[].class); + + Assert.assertNotNull(foundUsers); + Assert.assertEquals(savedUsers.size(), foundUsers.length); + + Map foundById = Arrays.stream(foundUsers) + .collect(Collectors.toMap(u -> u.getId().getId(), Function.identity())); + + for (User savedUser : savedUsers) { + User foundUser = foundById.get(savedUser.getId().getId()); + Assert.assertNotNull("User not found for id " + savedUser.getId().getId(), foundUser); + + foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); + Assert.assertEquals(savedUser, foundUser); + } + } + + @Test public void testSaveUserWithSameEmail() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java index 3440f26ea8..0b1c2d282e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java @@ -33,8 +33,13 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -150,6 +155,37 @@ public class WidgetsBundleControllerTest extends AbstractControllerTest { Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle); } + @Test + public void testFindWidgetsBundlesByIds() throws Exception { + List savedWidgetsBundles = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("My widgets bundle " + i); + savedWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class)); + } + + String idsParam = savedWidgetsBundles.stream() + .map(wb -> wb.getId().getId().toString()) + .collect(Collectors.joining(",")); + + WidgetsBundle[] foundWidgetsBundles = + doGet("/api/widgetsBundles?widgetsBundleIds=" + idsParam, WidgetsBundle[].class); + + Assert.assertNotNull(foundWidgetsBundles); + Assert.assertEquals(savedWidgetsBundles.size(), foundWidgetsBundles.length); + + Map foundById = Arrays.stream(foundWidgetsBundles) + .collect(Collectors.toMap(wb -> wb.getId().getId(), Function.identity())); + + for (WidgetsBundle savedWidgetsBundle : savedWidgetsBundles) { + UUID id = savedWidgetsBundle.getId().getId(); + WidgetsBundle foundWidgetsBundle = foundById.get(id); + Assert.assertNotNull("WidgetsBundle not found for id " + id, foundWidgetsBundle); + Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle); + } + } + + @Test public void testDeleteWidgetsBundle() throws Exception { WidgetsBundle widgetsBundle = new WidgetsBundle(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index bf29654941..e867656d4c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -62,4 +63,6 @@ public interface AssetProfileService extends EntityDaoService { List findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + ListenableFuture> findAssetProfilesByIdsAsync(TenantId tenantId, List assetProfileIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index d478e2099a..82a5605409 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; +import java.util.List; import java.util.Optional; public interface CustomerService extends EntityDaoService { @@ -47,4 +48,6 @@ public interface CustomerService extends EntityDaoService { void deleteCustomersByTenantId(TenantId tenantId); + ListenableFuture> findCustomersByTenantIdAndIdsAsync(TenantId tenantId, List customerIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 264a808c97..18efebc92d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -80,4 +80,6 @@ public interface DashboardService extends EntityDaoService { PageData findAllDashboardsIds(PageLink pageLink); + ListenableFuture> findDashboardInfoByIdsAsync(TenantId tenantId, List dashboardIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index 5a2b95bc1b..3bbd760eec 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.device; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityInfo; @@ -64,4 +65,6 @@ public interface DeviceProfileService extends EntityDaoService { List findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + ListenableFuture> findDeviceProfilesByIdsAsync(TenantId tenantId, List deviceProfileIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index a2356ee149..fd84cc4c82 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -117,4 +117,6 @@ public interface RuleChainService extends EntityDaoService { void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId); + ListenableFuture> findRuleChainsByIdsAsync(TenantId tenantId, List ruleChainIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index d70bcef209..db9b77e161 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -55,4 +55,6 @@ public interface TenantService extends EntityDaoService { PageData findTenantsIds(PageLink pageLink); + ListenableFuture> findTenantsByIdsAsync(TenantId callerId, List tenantIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 160e02fbc8..629f1c1db5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -110,4 +110,7 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); int countTenantAdmins(TenantId tenantId); + + ListenableFuture> findUsersByTenantIdAndIdsAsync(TenantId tenantId, List userIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java index 9cb699cca4..5341b8862b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.widget; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -52,4 +53,8 @@ public interface WidgetsBundleService extends EntityDaoService { void updateSystemWidgets(Stream bundles, Stream widgets); + ListenableFuture> findSystemWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds); + + ListenableFuture> findAllTenantWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index 290213b97d..f98a344460 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -31,7 +31,7 @@ import java.util.UUID; @Value @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true, exclude = "image") -public class DeviceProfileInfo extends EntityInfo { +public class DeviceProfileInfo extends EntityInfo implements HasTenantId { @Schema(description = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ") private final String image; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java index a530759d03..ae06f33825 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -23,6 +23,7 @@ import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -33,7 +34,7 @@ import java.util.UUID; @Value @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true, exclude = "image") -public class AssetProfileInfo extends EntityInfo { +public class AssetProfileInfo extends EntityInfo implements HasTenantId { @Schema(description = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ") private final String image; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java index da6acf6ca3..d56e794b93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -51,4 +52,6 @@ public interface AssetProfileDao extends Dao, ExportableEntityDao< List findTenantAssetProfileNames(UUID tenantId, boolean activeOnly); + ListenableFuture> findAssetProfilesByTenantIdAndIdsAsync(UUID tenantId, List assetProfileIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 4ddbabb5b6..63a5e1b241 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.asset; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -51,7 +52,9 @@ import java.util.Optional; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("AssetProfileDaoService") @Slf4j @@ -345,6 +348,14 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService> findAssetProfilesByIdsAsync(TenantId tenantId, List assetProfileIds) { + log.trace("Executing findAssetProfilesByIdsAsync, tenantId [{}], assetProfileIds [{}]", tenantId, assetProfileIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(assetProfileIds, ids -> "Incorrect assetProfileIds " + ids); + return assetProfileDao.findAssetProfilesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetProfileIds)); + } + private final PaginatedRemover tenantAssetProfilesRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index c6bfa970a0..9b3a22eaaa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.customer; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -24,6 +25,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -78,4 +80,6 @@ public interface CustomerDao extends Dao, TenantEntityDao, E */ PageData findCustomersWithTheSameTitle(PageLink pageLink); + ListenableFuture> findCustomersByTenantIdAndIdsAsync(UUID tenantId, List customerIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 9aee422030..10299e3223 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -55,9 +55,11 @@ import org.thingsboard.server.dao.user.UserService; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("CustomerDaoService") @Slf4j @@ -263,6 +265,14 @@ public class CustomerServiceImpl extends AbstractCachedEntityService> findCustomersByTenantIdAndIdsAsync(TenantId tenantId, List customerIds) { + log.trace("Executing findCustomersByTenantIdAndIdsAsync, tenantId [{}], customerIds [{}]", tenantId, customerIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(customerIds, ids -> "Incorrect customerIds " + ids); + return customerDao.findCustomersByTenantIdAndIdsAsync(tenantId.getId(), customerIds.stream().map(CustomerId::getId).collect(Collectors.toList())); + } + @Override public void deleteByTenantId(TenantId tenantId) { deleteCustomersByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 6a3ec3295e..3687c01d02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -22,6 +23,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.ResourceContainerDao; +import java.util.List; import java.util.UUID; /** @@ -81,4 +83,6 @@ public interface DashboardInfoDao extends Dao, ImageContainerDao< String findTitleById(UUID tenantId, UUID dashboardId); + ListenableFuture> findDashboardsByIdsAsync(UUID tenantId, List dashboardIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 9d8abe467c..7838c1a717 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -63,7 +63,9 @@ import java.util.Map; import java.util.Optional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("DashboardDaoService") @Slf4j @@ -409,6 +411,13 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardDao.findAllIds(pageLink); } + @Override + public ListenableFuture> findDashboardInfoByIdsAsync(TenantId tenantId, List dashboardIds) { + log.trace("Executing findDashboardInfoByIdsAsync, dashboardIds [{}]", dashboardIds); + validateIds(dashboardIds, ids -> "Incorrect dashboardIds " + ids); + return dashboardInfoDao.findDashboardsByIdsAsync(tenantId.getId(), toUUIDs(dashboardIds)); + } + private final PaginatedRemover tenantDashboardsRemover = new PaginatedRemover<>() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index f2c021ea8a..470881a690 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.device; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityInfo; @@ -53,4 +54,6 @@ public interface DeviceProfileDao extends Dao, ExportableEntityDa List findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly); + ListenableFuture> findDeviceProfilesByTenantIdAndIdsAsync(UUID tenantId, List deviceProfileIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index f782585888..eddf18e3c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -66,7 +67,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validateString; @Service("DeviceProfileDaoService") @@ -391,6 +394,14 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService> findDeviceProfilesByIdsAsync(TenantId tenantId, List deviceProfileIds) { + log.trace("Executing findDeviceProfilesByIdsAsync, tenantId [{}], deviceProfileIds [{}]", tenantId, deviceProfileIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(deviceProfileIds, ids -> "Incorrect deviceProfileIds " + ids); + return deviceProfileDao.findDeviceProfilesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceProfileIds)); + } + private final PaginatedRemover tenantDeviceProfilesRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8912e47c5b..c1561084b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -85,6 +85,7 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.DataConstants.TENANT; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -869,6 +870,14 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } + @Override + public ListenableFuture> findRuleChainsByIdsAsync(TenantId tenantId, List ruleChainIds) { + log.trace("Executing findRuleChainsByIdsAsync, tenantId [{}], ruleChainIds [{}]", tenantId, ruleChainIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(ruleChainIds, ids -> "Incorrect ruleChainIds " + ids); + return ruleChainDao.findRuleChainsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(ruleChainIds)); + } + @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index ac716bb4fc..7b4e9a7fb9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.rule; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -28,6 +29,7 @@ import org.thingsboard.server.dao.ResourceContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; +import java.util.List; import java.util.UUID; /** @@ -83,4 +85,6 @@ public interface RuleChainDao extends Dao, TenantEntityDao Collection findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name); + ListenableFuture> findRuleChainsByTenantIdAndIdsAsync(UUID tenantId, List ruleChainIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index eb35a4e18e..a9e707ebf2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -87,4 +87,10 @@ public interface AssetProfileRepository extends JpaRepository :id ORDER BY a.id") List findNextBatch(@Param("id") UUID id, Limit limit); + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a WHERE " + + "a.tenantId = :tenantId AND a.id IN :assetProfileIds") + List findAssetProfileInfosByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, + @Param("assetProfileIds") List assetProfileIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index eeab5a338a..99093827e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.asset; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.domain.PageRequest; @@ -105,6 +106,11 @@ public class JpaAssetProfileDao extends JpaAbstractDao> findAssetProfilesByTenantIdAndIdsAsync(UUID tenantId, List assetProfileIds) { + return service.submit(() -> assetProfileRepository.findAssetProfileInfosByTenantIdAndIdIn(tenantId, assetProfileIds)); + } + @Override public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index 8ad7311423..d53296b9ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -62,4 +62,6 @@ public interface CustomerRepository extends JpaRepository, "c.title, c.version, c.additionalInfo, c.country, c.state, c.city, c.address, c.address2, c.zip, c.phone, c.email) " + "FROM CustomerEntity c WHERE c.id > :id ORDER BY c.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + List findCustomersByTenantIdAndIdIn(UUID tenantId, List customerIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index 75e7179391..d1a31faaaf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.customer; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -107,6 +108,11 @@ public class JpaCustomerDao extends JpaAbstractDao imp ); } + @Override + public ListenableFuture> findCustomersByTenantIdAndIdsAsync(UUID tenantId, List customerIds) { + return service.submit(() -> DaoUtil.convertDataList(customerRepository.findCustomersByTenantIdAndIdIn(tenantId, customerIds))); + } + @Override public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { return findByTenantId(tenantId.getId(), pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 32ac596562..2198869c24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; @@ -99,4 +100,6 @@ public interface DashboardInfoRepository extends JpaRepository findDashboardInfosByResourceLink(@Param("link") String link, Pageable pageable); + List findByIdIn(List dashboardIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 0e04c94a46..f3999a10c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.dashboard; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; @@ -126,6 +127,11 @@ public class JpaDashboardInfoDao extends JpaAbstractDao> findDashboardsByIdsAsync(UUID tenantId, List dashboardIds) { + return service.submit(() -> DaoUtil.convertDataList(dashboardInfoRepository.findByIdIn(dashboardIds))); + } + @Override public List findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) { return DaoUtil.convertDataList(dashboardInfoRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, limit)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 88d4780f9d..4e8a536dd1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -59,6 +59,11 @@ public interface DeviceProfileRepository extends JpaRepository findDeviceProfileInfosByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("deviceProfileIds") List deviceProfileIds); + @Query("SELECT d FROM DeviceProfileEntity d " + "WHERE d.tenantId = :tenantId AND d.isDefault = true") DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index ebd8c78ed0..41dbf3978e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.device; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.domain.PageRequest; @@ -123,6 +124,11 @@ public class JpaDeviceProfileDao extends JpaAbstractDao> findDeviceProfilesByTenantIdAndIdsAsync(UUID tenantId, List deviceProfileIds) { + return service.submit(() -> deviceProfileRepository.findDeviceProfileInfosByTenantIdAndIdIn(tenantId, deviceProfileIds)); + } + @Override public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 4a6427a7e5..f541627309 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.rule; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; @@ -112,6 +113,11 @@ public class JpaRuleChainDao extends JpaAbstractDao return DaoUtil.convertDataList(ruleChainRepository.findByTenantIdAndTypeAndName(tenantId.getId(), type, name)); } + @Override + public ListenableFuture> findRuleChainsByTenantIdAndIdsAsync(UUID tenantId, List ruleChainIds) { + return service.submit(() -> DaoUtil.convertDataList(ruleChainRepository.findRuleChainsByTenantIdAndIdIn(tenantId, ruleChainIds))); + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index 4bf648cbbd..2dbb61f07c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -90,4 +90,7 @@ public interface RuleChainRepository extends JpaRepository :id ORDER BY r.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + List findRuleChainsByTenantIdAndIdIn(UUID tenantId, List ruleChainIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index fb558dab0c..781753bb7d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.tenant; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -98,6 +99,11 @@ public class JpaTenantDao extends JpaAbstractDao implement return DaoUtil.getData(tenantRepository.findFirstByTitle(name)); } + @Override + public ListenableFuture> findTenantsByIdsAsync(UUID tenantId, List tenantIds) { + return service.submit(() -> DaoUtil.convertDataList(tenantRepository.findTenantsByIdIn(tenantIds))); + } + @Override public List findNextBatch(UUID id, int batchSize) { return tenantRepository.findNextBatch(id, Limit.of(batchSize)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 08859d5f63..db10d971a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -62,4 +62,6 @@ public interface TenantRepository extends JpaRepository { TenantEntity findFirstByTitle(String name); + List findTenantsByIdIn(List tenantIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 753955089c..3b3f4dc15c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.user; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -141,6 +142,11 @@ public class JpaUserDao extends JpaAbstractDao implements User return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN); } + @Override + public ListenableFuture> findUsersByTenantIdAndIdsAsync(UUID tenantId, List userIds) { + return service.submit(() -> DaoUtil.convertDataList(userRepository.findUsersByTenantIdAndIdIn(tenantId, userIds))); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 2254377af3..5dacf2ad09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -80,4 +80,7 @@ public interface UserRepository extends JpaRepository { List findNextBatch(@Param("id") UUID id, Limit limit); int countByTenantIdAndAuthority(UUID tenantId, Authority authority); + + List findUsersByTenantIdAndIdIn(UUID tenantId, List userIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index bc81be054a..b1481d636a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sql.widget; +import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -111,6 +112,17 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao> findSystemWidgetBundlesByIdsAsync(UUID tenantId, List widgetsBundleIds) { + return service.submit(() -> DaoUtil.convertDataList(widgetsBundleRepository.findSystemWidgetsBundlesByIdIn(NULL_UUID, widgetsBundleIds))); + } + + @Override + public ListenableFuture> findAllTenantWidgetBundlesByTenantIdAndIdsAsync(UUID tenantId, List widgetsBundleIds) { + return service.submit(() -> DaoUtil.convertDataList(widgetsBundleRepository + .findAllTenantWidgetsBundlesByTenantIdAndIdIn(tenantId, NULL_UUID, widgetsBundleIds))); + } + private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { if (widgetsBundleFilter.isFullSearch()) { return DaoUtil.toPageData( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index de778588dd..53053eb3c0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -146,4 +146,16 @@ public interface WidgetsBundleRepository extends JpaRepository :id ORDER BY w.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :systemTenantId " + + "AND wb.id IN :widgetsBundleIds") + List findSystemWidgetsBundlesByIdIn(@Param("systemTenantId") UUID systemTenantId, + @Param("widgetsBundleIds") List widgetsBundleIds); + + @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId IN (:tenantId, :nullTenantId) " + + "AND wb.id IN :widgetsBundleIds") + List findAllTenantWidgetsBundlesByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, + @Param("nullTenantId") UUID nullTenantId, + @Param("widgetsBundleIds") List widgetsBundleIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index 8ff3999c4e..5bab7e6295 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; @@ -42,4 +43,6 @@ public interface TenantDao extends Dao { Tenant findTenantByName(TenantId tenantId, String name); + ListenableFuture> findTenantsByIdsAsync(UUID tenantId, List tenantIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index bd5a621562..a6f8bac498 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -53,7 +53,9 @@ import java.util.Optional; import java.util.function.Consumer; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("TenantDaoService") @Slf4j @@ -224,6 +226,13 @@ public class TenantServiceImpl extends AbstractCachedEntityService> findTenantsByIdsAsync(TenantId callerId, List tenantIds) { + log.trace("Executing findTenantsByIdsAsync, callerId [{}], tenantIds [{}]", callerId, tenantIds); + validateIds(tenantIds, ids -> "Incorrect tenantIds " + ids); + return tenantDao.findTenantsByIdsAsync(callerId.getId(), toUUIDs(tenantIds)); + } + @Override public boolean tenantExists(TenantId tenantId) { return existsTenantCache.getAndPutInTransaction(tenantId, () -> tenantDao.existsById(tenantId, tenantId.getId()), false); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index 127aa6141a..26e98a4edf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.user; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -102,4 +103,7 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); int countTenantAdmins(UUID tenantId); + + ListenableFuture> findUsersByTenantIdAndIdsAsync(UUID tenantId, List userIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 595a2a6611..dbf3c72c5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -75,7 +75,9 @@ import java.util.concurrent.TimeUnit; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -492,6 +494,14 @@ public class UserServiceImpl extends AbstractCachedEntityService> findUsersByTenantIdAndIdsAsync(TenantId tenantId, List userIds) { + log.trace("Executing findUsersByTenantIdAndIdsAsync, tenantId [{}], userIds [{}]", tenantId, userIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(userIds, ids -> "Incorrect userIds " + ids); + return userDao.findUsersByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(userIds)); + } + private Optional findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index 18a63944e8..b9e03072b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.widget; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -25,6 +26,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import java.util.List; import java.util.UUID; /** @@ -87,5 +89,9 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa PageData findAllWidgetsBundles(PageLink pageLink); + ListenableFuture> findSystemWidgetBundlesByIdsAsync(UUID tenantId, List widgetsBundleIds); + + ListenableFuture> findAllTenantWidgetBundlesByTenantIdAndIdsAsync(UUID tenantId, List widgetsBundleIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 5b5930fb0a..5d4b6d4bb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.widget; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -48,7 +49,10 @@ import java.util.Optional; import java.util.stream.Stream; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("WidgetsBundleDaoService") @Slf4j @@ -252,6 +256,22 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { }); } + @Override + public ListenableFuture> findSystemWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds) { + log.trace("Executing findSystemWidgetsBundlesByIdsAsync, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(widgetsBundleIds, ids -> "Incorrect widgetsBundleIds " + ids); + return widgetsBundleDao.findSystemWidgetBundlesByIdsAsync(tenantId.getId(), toUUIDs(widgetsBundleIds)); + } + + @Override + public ListenableFuture> findAllTenantWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds) { + log.trace("Executing findAllTenantWidgetsBundlesByIdsAsync, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(widgetsBundleIds, ids -> "Incorrect widgetsBundleIds " + ids); + return widgetsBundleDao.findAllTenantWidgetBundlesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(widgetsBundleIds)); + } + private WidgetTypeDetails updateSystemWidget(JsonNode widgetTypeJson) { WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); WidgetType existingWidget = widgetTypeService.findWidgetTypeByTenantIdAndFqn(TenantId.SYS_TENANT_ID, widgetTypeDetails.getFqn()); From 0af925433a91525d25cc2f64c13f24c477db4d39 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Nov 2025 18:35:23 +0200 Subject: [PATCH 004/147] minor improvements --- .../controller/AssetProfileController.java | 17 +++++------------ .../controller/DeviceProfileController.java | 16 +++++----------- .../permission/AccessControlService.java | 2 ++ .../permission/DefaultAccessControlService.java | 10 ++++++++++ .../security/permission/PermissionChecker.java | 1 - .../server/common/data/DeviceProfileInfo.java | 2 +- .../common/data/asset/AssetProfileInfo.java | 3 +-- 7 files changed, 24 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 9415e7194e..72223f129a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -48,8 +48,8 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; @@ -236,7 +236,9 @@ public class AssetProfileController extends BaseController { @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("assetProfileIds") String[] strAssetProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("assetProfileIds", strAssetProfileIds); - + if (!accessControlService.hasPermission(getCurrentUser(), Resource.ASSET_PROFILE, Operation.READ)) { + return Collections.emptyList(); + } SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); List assetProfileIds = new ArrayList<>(); @@ -244,16 +246,7 @@ public class AssetProfileController extends BaseController { assetProfileIds.add(new AssetProfileId(toUUID(strAssetProfileId))); } - return Objects.requireNonNull(checkNotNull(assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.ASSET_PROFILE, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + return checkNotNull(assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get()); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 7f6f6bfa61..fd0cafb3ec 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -51,8 +51,8 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -292,6 +292,9 @@ public class DeviceProfileController extends BaseController { @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("deviceProfileIds") String[] strDeviceProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("deviceProfileIds", strDeviceProfileIds); + if (!accessControlService.hasPermission(getCurrentUser(), Resource.DEVICE_PROFILE, Operation.READ)) { + return Collections.emptyList(); + } SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); List deviceProfileIds = new ArrayList<>(); @@ -299,16 +302,7 @@ public class DeviceProfileController extends BaseController { deviceProfileIds.add(new DeviceProfileId(toUUID(strDeviceProfileId))); } - return Objects.requireNonNull(checkNotNull(deviceProfileService.findDeviceProfilesByIdsAsync(tenantId, deviceProfileIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.DEVICE_PROFILE, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + return checkNotNull(deviceProfileService.findDeviceProfilesByIdsAsync(tenantId, deviceProfileIds).get()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java index b760a54ade..1d8d0992ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java @@ -24,6 +24,8 @@ public interface AccessControlService { void checkPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException; + boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException; + void checkPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index 9bf05e2d73..f8898f9660 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -55,6 +55,16 @@ public class DefaultAccessControlService implements AccessControlService { } } + @Override + @SuppressWarnings("unchecked") + public boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException { + PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); + if (permissionChecker != null) { + return permissionChecker.hasPermission(user, operation); + } + return false; + } + @Override @SuppressWarnings("unchecked") public void checkPermission(SecurityUser user, Resource resource, diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java index 216d74fe2f..2fcf6b71fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/PermissionChecker.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.permission; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index f98a344460..290213b97d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -31,7 +31,7 @@ import java.util.UUID; @Value @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true, exclude = "image") -public class DeviceProfileInfo extends EntityInfo implements HasTenantId { +public class DeviceProfileInfo extends EntityInfo { @Schema(description = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ") private final String image; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java index ae06f33825..a530759d03 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -23,7 +23,6 @@ import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -34,7 +33,7 @@ import java.util.UUID; @Value @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true, exclude = "image") -public class AssetProfileInfo extends EntityInfo implements HasTenantId { +public class AssetProfileInfo extends EntityInfo { @Schema(description = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ") private final String image; From b56e017ff1a5c7f15876adcb61f92df5abb32175 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Nov 2025 18:57:39 +0200 Subject: [PATCH 005/147] minor improvements --- .../server/controller/AssetProfileController.java | 4 ++-- .../server/controller/DeviceProfileController.java | 4 ++-- .../service/security/permission/TenantAdminPermissions.java | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 72223f129a..8f0b7480ab 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -236,10 +236,10 @@ public class AssetProfileController extends BaseController { @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("assetProfileIds") String[] strAssetProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("assetProfileIds", strAssetProfileIds); - if (!accessControlService.hasPermission(getCurrentUser(), Resource.ASSET_PROFILE, Operation.READ)) { + SecurityUser user = getCurrentUser(); + if (!accessControlService.hasPermission(user, Resource.ASSET_PROFILE, Operation.READ)) { return Collections.emptyList(); } - SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); List assetProfileIds = new ArrayList<>(); for (String strAssetProfileId : strAssetProfileIds) { diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index fd0cafb3ec..7092a48c59 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -292,10 +292,10 @@ public class DeviceProfileController extends BaseController { @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("deviceProfileIds") String[] strDeviceProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("deviceProfileIds", strDeviceProfileIds); - if (!accessControlService.hasPermission(getCurrentUser(), Resource.DEVICE_PROFILE, Operation.READ)) { + SecurityUser user = getCurrentUser(); + if (!accessControlService.hasPermission(user, Resource.DEVICE_PROFILE, Operation.READ)) { return Collections.emptyList(); } - SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); List deviceProfileIds = new ArrayList<>(); for (String strDeviceProfileId : strDeviceProfileIds) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 7a824ca735..9baf24e7d9 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -63,6 +63,11 @@ public class TenantAdminPermissions extends AbstractPermissions { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + @Override public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!user.getTenantId().equals(entity.getTenantId())) { From 4ece91a0fd0915964157ff93997f43f9cc891a0d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 21 Nov 2025 13:48:17 +0200 Subject: [PATCH 006/147] lwm2m: fix bug progileID is null after reboot if sleep --- .../server/client/LwM2mClientContextImpl.java | 2 +- .../ota/DefaultLwM2MOtaUpdateService.java | 58 ++++++++++--------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 4cf8d825b9..f5165f47c8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -360,7 +360,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration) { UUID profileId = getClientByEndpoint(registration.getEndpoint()).getProfileId(); - return doGetAndCache(profileId); + return profileId != null ? doGetAndCache(profileId) : null; } private Lwm2mDeviceProfileTransportConfiguration doGetAndCache(UUID profileId) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 79f077a71e..325f1e2280 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -201,35 +201,39 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); - initFwStrategy(client, clientSettings); - initSwStrategy(client, clientSettings); - - if (!attributesToFetch.isEmpty()) { - var future = attributesService.getSharedAttributes(client, attributesToFetch); - DonAsynchron.withCallback(future, attrs -> { - if (fwInfo.isSupported()) { - Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); - Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); - Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); - Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); - if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { - onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + if (clientSettings != null) { + initFwStrategy(client, clientSettings); + initSwStrategy(client, clientSettings); + + + if (!attributesToFetch.isEmpty()) { + var future = attributesService.getSharedAttributes(client, attributesToFetch); + DonAsynchron.withCallback(future, attrs -> { + if (fwInfo.isSupported()) { + Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); + Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); + Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); + Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); + if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { + onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + } } - } - if (swInfo.isSupported()) { - Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); - Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); - Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); - Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); - if (newSwTitle.isPresent() && newSwVersion.isPresent()) { - onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + if (swInfo.isSupported()) { + Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); + Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); + Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); + Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); + if (newSwTitle.isPresent() && newSwVersion.isPresent()) { + onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + } } - } - }, throwable -> { - if (fwInfo.isSupported()) { - update(fwInfo); - } - }, executor); + + }, throwable -> { + if (fwInfo.isSupported()) { + update(fwInfo); + } + }, executor); + } } } From f398a81a429f1163107ace6c2e85d7aa13034445 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 21 Nov 2025 19:15:27 +0200 Subject: [PATCH 007/147] lwm2m: fix bug redis -> Cleaner if registration == null --- .../server/store/TbLwM2mRedisRegistrationStore.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index df432732b2..3c9828af22 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -750,11 +750,14 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab System.currentTimeMillis(), 0, cleanLimit); for (byte[] endpoint : endpointsExpired) { - Registration r = deserializeReg(connection.get(toEndpointKey(endpoint))); - if (!r.isAlive(gracePeriod)) { - Deregistration dereg = removeRegistration(connection, r.getId(), true); - if (dereg != null) - expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + byte[] data = connection.get(toEndpointKey(endpoint)); + if (data != null && data.length > 0) { + Registration r = deserializeReg(data); + if (!r.isAlive(gracePeriod)) { + Deregistration dereg = removeRegistration(connection, r.getId(), true); + if (dereg != null) + expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + } } } } catch (Exception e) { From 0dd37617e1fadb8b058ec8b17c4ce09aa900398a Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Wed, 26 Nov 2025 15:19:22 +0200 Subject: [PATCH 008/147] added get entity views by ids endpoint --- .../controller/EntityViewController.java | 49 +++++++++++++------ .../controller/EntityViewControllerTest.java | 33 +++++++++++++ .../dao/entityview/EntityViewService.java | 2 + .../server/dao/entityview/EntityViewDao.java | 2 + .../dao/entityview/EntityViewServiceImpl.java | 10 ++++ .../sql/entityview/EntityViewRepository.java | 2 + .../dao/sql/entityview/JpaEntityViewDao.java | 5 ++ 7 files changed, 87 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index b1b6b6b1e3..c67f260422 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -17,11 +17,13 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -53,6 +55,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -347,14 +350,7 @@ public class EntityViewController extends BaseController { checkNotNull(query.getEntityViewTypes()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); List entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(getTenantId(), query).get()); - entityViews = entityViews.stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + entityViews = filterEntityViewsByReadPermission(entityViews); return entityViews; } @@ -459,18 +455,39 @@ public class EntityViewController extends BaseController { } else { nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); } - List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); - return true; - } catch (ThingsboardException e) { - return false; - } - }).collect(Collectors.toList()); + List filteredEntityViews = filterEntityViewsByReadPermission(nonFilteredResult.getData()); PageData filteredResult = new PageData<>(filteredEntityViews, nonFilteredResult.getTotalPages(), nonFilteredResult.getTotalElements(), nonFilteredResult.hasNext()); return checkNotNull(filteredResult); } + + @ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)", + notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/entityViews", params = {"entityViewIds"}) + public List getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("entityViewIds") String[] strEntityViewIds) throws ThingsboardException, ExecutionException, InterruptedException { + checkArrayParameter("entityViewIds", strEntityViewIds); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List entityViewIds = new ArrayList<>(); + for (String strEntityViewId : strEntityViewIds) { + entityViewIds.add(new EntityViewId(toUUID(strEntityViewId))); + } + List entityViews = checkNotNull(entityViewService.findEntityViewsByTenantIdAndIdsAsync(tenantId, entityViewIds).get()); + return filterEntityViewsByReadPermission(entityViews); + } + + private List filterEntityViewsByReadPermission(List entityViews) { + return entityViews.stream().filter(entityView -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index 10b49dfe1d..5b4c9d017b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -68,11 +68,14 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import static java.util.concurrent.TimeUnit.HOURS; @@ -148,6 +151,36 @@ public class EntityViewControllerTest extends AbstractControllerTest { assertEquals(savedView, foundView); } + @Test + public void testFindEntityViewByIds() throws Exception { + List assetProfiles = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + assetProfiles.add(getNewSavedEntityView("Test entity view " + i)); + } + + List expected = assetProfiles.subList(5, 15); + + String idsParam = expected.stream() + .map(ap -> ap.getId().getId().toString()) + .collect(Collectors.joining(",")); + EntityView[] foundEntityViews = doGet("/api/entityViews?entityViewIds=" + idsParam, EntityView[].class); + + Assert.assertNotNull(foundEntityViews); + Assert.assertEquals(expected.size(), foundEntityViews.length); + + Map infoById = Arrays.stream(foundEntityViews) + .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity())); + + for (EntityView entityView : expected) { + UUID id = entityView.getId().getId(); + EntityView view = infoById.get(id); + Assert.assertNotNull("Entity view not found for id " + id, view); + + Assert.assertEquals(entityView.getId(), view.getId()); + Assert.assertEquals(entityView.getName(), view.getName()); + } + } + @Test public void testSaveEntityView() throws Exception { String name = "Test entity view"; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 5ec2dfc20e..e64cd0dd6d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -65,6 +65,8 @@ public interface EntityViewService extends EntityDaoService { PageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + ListenableFuture> findEntityViewsByTenantIdAndIdsAsync(TenantId tenantId, List entityViewIds); + PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); PageData findEntityViewsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, PageLink pageLink, String type); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index 17f84c7b06..52d077a3bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -169,6 +169,8 @@ public interface EntityViewDao extends Dao, ExportableEntityDao> findEntityViewsByTenantIdAndIdsAsync(UUID tenantId, List entityViewIds); + /** * Find entity views by tenantId, edgeId, type and page link. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 3128036e0e..e135555659 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -62,7 +62,9 @@ import java.util.Optional; import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -255,6 +257,14 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService> findEntityViewsByTenantIdAndIdsAsync(TenantId tenantId, List entityViewIds) { + log.trace("Executing findEntityViewsByTenantIdAndIdsAsync, tenantId [{}], entityViewIds [{}]", tenantId, entityViewIds); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateIds(entityViewIds, ids -> "Incorrect entityViewIds " + ids); + return entityViewDao.findEntityViewsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(entityViewIds)); + } + @Override public PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findEntityViewInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 6094e9b171..c61ec83335 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -122,6 +122,8 @@ public interface EntityViewRepository extends JpaRepository findEntityViewsByTenantIdAndIdIn(UUID tenantId, List entityViewIds); + @Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :tenantId") List findTenantEntityViewTypes(@Param("tenantId") UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 44d8a09ff4..fe3f63825f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -187,6 +187,11 @@ public class JpaEntityViewDao extends JpaAbstractDao> findEntityViewsByTenantIdAndIdsAsync(UUID tenantId, List entityViewIds) { + return service.submit(() -> DaoUtil.convertDataList(entityViewRepository.findEntityViewsByTenantIdAndIdIn(tenantId, entityViewIds))); + } + @Override public PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { log.debug("Try to find entity views by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); From 4a6177d85e94c3c221be5d9fdbeae73dde547479 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 27 Nov 2025 23:14:17 +0100 Subject: [PATCH 009/147] monitoring service improvements: provisioning dashboard, make monitoring asset and dashboard public, log dashboard url, notify on startup and shutdown, shutdown thread pools on PreDestroy, removed file based logback in favour of stdout, logback severity down to INFO, JAVA_OPTS adjusted --- monitoring/src/main/conf/logback.xml | 20 +- monitoring/src/main/conf/tb-monitoring.conf | 7 +- .../ThingsboardMonitoringApplication.java | 37 +- .../data/notification/InfoNotification.java | 27 + .../notification/NotificationService.java | 29 +- .../service/MonitoringEntityService.java | 120 +++- .../resources/dashboard_cloud_monitoring.json | 580 ++++++++++++++++++ monitoring/src/main/resources/logback.xml | 3 +- 8 files changed, 772 insertions(+), 51 deletions(-) create mode 100644 monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java create mode 100644 monitoring/src/main/resources/dashboard_cloud_monitoring.json diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index 53caafe8bb..43001ffd43 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -19,20 +19,6 @@ - - ${pkg.logFolder}/${pkg.name}.log - - ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n @@ -40,11 +26,11 @@ - - + + + - diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf index 493d498cd8..cd2947563f 100644 --- a/monitoring/src/main/conf/tb-monitoring.conf +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -14,9 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=3,filesize=10M" export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" -export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-monitoring.out export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index c8e8d7070b..f225d0d771 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -15,36 +15,43 @@ */ package org.thingsboard.monitoring; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.annotation.EnableScheduling; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.monitoring.data.notification.InfoNotification; +import org.thingsboard.monitoring.notification.NotificationService; import org.thingsboard.monitoring.service.BaseMonitoringService; import org.thingsboard.monitoring.service.MonitoringEntityService; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j +@RequiredArgsConstructor public class ThingsboardMonitoringApplication { - @Autowired - private List> monitoringServices; - @Autowired - private MonitoringEntityService entityService; + private final List> monitoringServices; + private final MonitoringEntityService entityService; + private final NotificationService notificationService; @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) @@ -56,12 +63,30 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); scheduler.scheduleWithFixedDelay(() -> { monitoringServices.forEach(monitoringService -> { monitoringService.runChecks(); }); }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + notificationService.sendNotification(new InfoNotification(":rocket: Monitoring started")); + } + + @EventListener(ContextClosedEvent.class) + public void onShutdown(ContextClosedEvent event) { + log.info("Shutting down monitoring service"); + try { + var futures = notificationService.sendNotification(new InfoNotification(":warning: Monitoring is shutting down")); + for (Future future : futures) { + future.get(5, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.warn("Failed to send shutdown notification", e); + } + } + + @PreDestroy + public void shutdownScheduler() { + scheduler.shutdown(); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java new file mode 100644 index 0000000000..6aa42cbf2b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.data.notification; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class InfoNotification implements Notification { + private final String message; + @Override + public String getText() { + return message; + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java index 5e4dcb2f34..3a496fc016 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -22,10 +22,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.monitoring.data.notification.Notification; import org.thingsboard.monitoring.notification.channels.NotificationChannel; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; @Component @RequiredArgsConstructor @@ -38,22 +41,38 @@ public class NotificationService { @Value("${monitoring.notifications.message_prefix}") private String messagePrefix; - public void sendNotification(Notification notification) { + public List> sendNotification(Notification notification) { String message; if (StringUtils.isEmpty(messagePrefix)) { message = notification.getText(); } else { - message = messagePrefix + System.lineSeparator() + notification.getText(); + message = messagePrefix + " " + notification.getText(); } - notificationChannels.forEach(notificationChannel -> { + return notificationChannels.stream().map(notificationChannel -> notificationExecutor.submit(() -> { try { notificationChannel.sendNotification(message); } catch (Exception e) { log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); } - }); - }); + }) + ).toList(); } + @PreDestroy + public void shutdownExecutor() { + try { + notificationExecutor.shutdown(); + if (!notificationExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + java.util.List dropped = notificationExecutor.shutdownNow(); + log.warn("Notification executor did not terminate in time. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } + } catch (InterruptedException e) { + java.util.List dropped = notificationExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + log.warn("Interrupted during notification executor shutdown. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } catch (Exception e) { + log.warn("Unexpected error while shutting down notification executor", e); + } + } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index 062104ecd5..fad9bc6b7c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -30,13 +30,19 @@ import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.util.ResourceUtils; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -57,7 +63,6 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -66,6 +71,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_CF_TELEMETRY_KEY; @@ -76,6 +83,9 @@ import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_TELEMETR @RequiredArgsConstructor public class MonitoringEntityService { + private static final String DASHBOARD_TITLE = "[Monitoring] Cloud monitoring"; + private static final String DASHBOARD_RESOURCE_PATH = "dashboard_cloud_monitoring.json"; + private final TbClient tbClient; @Value("${monitoring.calculated_fields.enabled:true}") @@ -95,27 +105,34 @@ public class MonitoringEntityService { int currentVersion = Integer.parseInt(attributes.getOrDefault("version", "0")); int newVersion = ruleChainDescriptor.get("version").asInt(); if (currentVersion == newVersion) { - log.info("Not updating rule chain, version is the same ({})", currentVersion); - return; + log.debug("Not updating rule chain, version is the same ({})", currentVersion); } else { log.info("Updating rule chain '{}' from version {} to {}", ruleChain.getName(), currentVersion, newVersion); + + String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), + "\\$\\{MONITORING:(.+?)}", matchResult -> { + String key = matchResult.group(1); + String value = attributes.get(key); + if (value == null) { + throw new IllegalArgumentException("No attribute found for key " + key); + } + log.info("Using {}: {}", key, value); + return value; + }); + RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); + metaData.setRuleChainId(ruleChainId); + tbClient.saveRuleChainMetaData(metaData); + tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() + .put("version", newVersion)); } - String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), - "\\$\\{MONITORING:(.+?)}", matchResult -> { - String key = matchResult.group(1); - String value = attributes.get(key); - if (value == null) { - throw new IllegalArgumentException("No attribute found for key " + key); - } - log.info("Using {}: {}", key, value); - return value; - }); - RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); - metaData.setRuleChainId(ruleChainId); - tbClient.saveRuleChainMetaData(metaData); - tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() - .put("version", newVersion)); + Asset asset = getOrCreateMonitoringAsset(); + Dashboard dashboard = getOrCreateMonitoringDashboard(); + + tbClient.assignAssetToPublicCustomer(asset.getId()); + tbClient.assignDashboardToPublicCustomer(dashboard.getId()); + + getDashboardPublicLink(dashboard); } public Asset getOrCreateMonitoringAsset() { @@ -249,4 +266,71 @@ public class MonitoringEntityService { tbClient.saveCalculatedField(calculatedField); } + private String getDashboardPublicLink(Dashboard dashboard) { + String link = ""; + try { + Optional infoOpt = tbClient.getDashboardInfoById(dashboard.getId()); + if (infoOpt.isPresent()) { + String publicCustomerId = null; + Set customers = infoOpt.get().getAssignedCustomers(); + if (customers != null) { + publicCustomerId = customers.stream() + .filter(ShortCustomerInfo::isPublic) + .map(c -> c.getCustomerId().getId().toString()) + .findFirst().orElse(null); + } + if (publicCustomerId != null) { + link = buildPublicDashboardLink(dashboard.getId(), publicCustomerId); + log.info("Public Monitoring dashboard link: {}", link); + } else { + log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); + } + } + } catch (Exception e) { + log.error("Failed to get a public link to Monitoring dashboard ", e); + } + return link; + } + + private Dashboard getOrCreateMonitoringDashboard() { + Dashboard existing = findDashboardByTitle(DASHBOARD_TITLE).orElse(null); + if (existing != null) { + log.debug("Found Monitoring dashboard '{}' with id {}", existing.getTitle(), existing.getId()); + return existing; + } + + Dashboard dashboardFromResource = ResourceUtils.getResource(DASHBOARD_RESOURCE_PATH, Dashboard.class); + dashboardFromResource.setTitle(DASHBOARD_TITLE); + //Optional.ofNullable(existing).map(Dashboard::getId).ifPresent(dashboardFromResource::setId); + Dashboard saved = tbClient.saveDashboard(dashboardFromResource); + log.info("Created Monitoring dashboard '{}' with id {}", saved.getTitle(), saved.getId()); + return saved; + } + + private Optional findDashboardByTitle(String title) { + // Use text search first and then filter by exact title + PageData page = tbClient.getTenantDashboards(new PageLink(10, 0, title)); + return page.getData().stream() + .filter(info -> title.equals(info.getTitle())) + .findFirst() + .flatMap(info -> tbClient.getDashboardById(info.getId())); + } + + private String buildPublicDashboardLink(DashboardId dashboardId, String publicCustomerId) { + String base = getBaseUrl(); + return String.format("%s/dashboard/%s?publicId=%s", base, dashboardId.getId().toString(), publicCustomerId); + } + + private String getBaseUrl() { + // TbClient.baseURL contains the root url, without trailing slash + try { + var baseUrlField = tbClient.getClass().getSuperclass().getDeclaredField("baseURL"); + baseUrlField.setAccessible(true); + return (String) baseUrlField.get(tbClient); + } catch (Exception e) { + log.warn("Unable to access baseURL from RestClient. Falling back to http://localhost:8080"); + return "http://localhost:8080"; + } + } + } diff --git a/monitoring/src/main/resources/dashboard_cloud_monitoring.json b/monitoring/src/main/resources/dashboard_cloud_monitoring.json new file mode 100644 index 0000000000..8be9677c61 --- /dev/null +++ b/monitoring/src/main/resources/dashboard_cloud_monitoring.json @@ -0,0 +1,580 @@ +{ + "title": "Cloud - Monitoring", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "arrivalLatency", + "type": "timeseries", + "label": "Arrival latency", + "color": "#ffc107", + "settings": {}, + "_hash": 0.7424467450112592, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "processingTime", + "type": "timeseries", + "label": "Processing time", + "color": "#607d8b", + "settings": {}, + "_hash": 0.6065128737901203, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "mqttTransportWsUpdateLatency", + "type": "timeseries", + "label": "MQTT - overall", + "color": "#f44336", + "settings": {}, + "_hash": 0.19638031054010696, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportWsUpdateLatency", + "type": "timeseries", + "label": "CoAP - overall", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8167481665043521, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportWsUpdateLatency", + "type": "timeseries", + "label": "HTTP - overall", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.45369210935861803, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportWsUpdateLatency", + "type": "timeseries", + "label": "LwM2M - overall", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.3477627917073993, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationWsUpdateLatency", + "type": "timeseries", + "label": "CoAP integration - overall", + "color": "#e91e63", + "settings": {}, + "_hash": 0.8332881819050183, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsSubscribeLatency", + "type": "timeseries", + "label": "WS subscribe latency", + "color": "#03a9f4", + "settings": {}, + "_hash": 0.7843180730206556, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsConnectLatency", + "type": "timeseries", + "label": "WS connect latency", + "color": "#4caf50", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.23140687573220564, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "logInLatency", + "type": "timeseries", + "label": "Log in latency", + "color": "#2196f3", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.6130919996800452, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 4, + "smoothLines": false, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [], + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "General latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "7db7f580-2aac-d7ee-20f6-3d9315e7003f", + "typeFullFqn": "system.charts.basic_timeseries" + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "mqttTransportRequestLatency", + "type": "timeseries", + "label": "MQTT transport request latency", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8576659620523571, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportRequestLatency", + "type": "timeseries", + "label": "HTTP transport request latency", + "color": "#4caf50", + "settings": {}, + "_hash": 0.9749033105491403, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportRequestLatency", + "type": "timeseries", + "label": "CoAP transport request latency", + "color": "#f44336", + "settings": {}, + "_hash": 0.6977575200148037, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationRequestLatency", + "type": "timeseries", + "label": "CoAP integration request latency", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.18839685494200875, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportRequestLatency", + "type": "timeseries", + "label": "LwM2M transport request latency", + "color": "#ffc107", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.85549409179514, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "shadowSize": 4, + "fontColor": "#545454", + "fontSize": 10, + "xaxis": { + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "showLabels": true, + "color": "#545454" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "legend": { + "show": true, + "position": "nw", + "backgroundColor": "#f0f0f0", + "backgroundOpacity": 0.85, + "labelBoxBorderColor": "rgba(1, 1, 1, 0.45)" + }, + "decimals": 1, + "stack": false, + "tooltipIndividual": false, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "Transport latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1", + "typeFullFqn": "system.charts.basic_timeseries" + } + }, + "states": { + "default": { + "name": "Cloud - Monitoring", + "root": true, + "layouts": { + "main": { + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 0 + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 12 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "outerMargin": true, + "layoutType": "default" + } + } + } + } + }, + "entityAliases": { + "6451461f-d748-9528-e3cd-64078c7fae05": { + "id": "6451461f-d748-9528-e3cd-64078c7fae05", + "alias": "Monitoring stats asset", + "filter": { + "type": "entityName", + "resolveMultiple": true, + "entityType": "ASSET", + "entityNameFilter": "[Monitoring] Latencies" + } + } + }, + "filters": {}, + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 120000, + "timewindowMs": 18000000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 600000, + "timewindowMs": 43200000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 2500 + }, + "timezone": null + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true + } + }, + "name": "Cloud - Monitoring", + "resources": null +} \ No newline at end of file diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml index df3a7224c6..43001ffd43 100644 --- a/monitoring/src/main/resources/logback.xml +++ b/monitoring/src/main/resources/logback.xml @@ -17,7 +17,7 @@ --> - + @@ -34,5 +34,4 @@ - From 9850e7a4664525f34f569a8efa12995d3ced3e4e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 28 Nov 2025 00:06:42 +0100 Subject: [PATCH 010/147] monitoring: show dashboard link on startup notification. Initial delay set for services to avoid spikes --- .../ThingsboardMonitoringApplication.java | 17 ++++++++++------- .../service/MonitoringEntityService.java | 10 ++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index f225d0d771..88e9ac1fa3 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -50,7 +50,7 @@ public class ThingsboardMonitoringApplication { @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring"); public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) @@ -63,12 +63,15 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - scheduler.scheduleWithFixedDelay(() -> { - monitoringServices.forEach(monitoringService -> { - monitoringService.runChecks(); - }); - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); - notificationService.sendNotification(new InfoNotification(":rocket: Monitoring started")); + for (int i = 0; i < monitoringServices.size(); i++) { + int initialDelay = (monitoringRateMs / monitoringServices.size()) * i; + BaseMonitoringService service = monitoringServices.get(i); + log.info("Scheduling initialDelay {}, fixedDelay {} for monitoring '{}' ", initialDelay, monitoringRateMs, service.getClass().getSimpleName()); + scheduler.scheduleWithFixedDelay(service::runChecks, initialDelay, monitoringRateMs, TimeUnit.MILLISECONDS); + } + + String publicDashboardUrl = entityService.getDashboardPublicLink(); + notificationService.sendNotification(new InfoNotification(":rocket: <"+publicDashboardUrl+"|Monitoring> started")); } @EventListener(ContextClosedEvent.class) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index fad9bc6b7c..a0991bce72 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -91,6 +91,8 @@ public class MonitoringEntityService { @Value("${monitoring.calculated_fields.enabled:true}") private boolean calculatedFieldsMonitoringEnabled; + DashboardId dashboardId = null; + public void checkEntities() { RuleChain ruleChain = tbClient.getRuleChains(RuleChainType.CORE, new PageLink(10)).getData().stream() .filter(RuleChain::isRoot) @@ -132,7 +134,7 @@ public class MonitoringEntityService { tbClient.assignAssetToPublicCustomer(asset.getId()); tbClient.assignDashboardToPublicCustomer(dashboard.getId()); - getDashboardPublicLink(dashboard); + this.dashboardId = Optional.ofNullable(dashboard).map(Dashboard::getId).orElse(null); } public Asset getOrCreateMonitoringAsset() { @@ -266,10 +268,10 @@ public class MonitoringEntityService { tbClient.saveCalculatedField(calculatedField); } - private String getDashboardPublicLink(Dashboard dashboard) { + public String getDashboardPublicLink() { String link = ""; try { - Optional infoOpt = tbClient.getDashboardInfoById(dashboard.getId()); + Optional infoOpt = tbClient.getDashboardInfoById(dashboardId); if (infoOpt.isPresent()) { String publicCustomerId = null; Set customers = infoOpt.get().getAssignedCustomers(); @@ -280,7 +282,7 @@ public class MonitoringEntityService { .findFirst().orElse(null); } if (publicCustomerId != null) { - link = buildPublicDashboardLink(dashboard.getId(), publicCustomerId); + link = buildPublicDashboardLink(dashboardId, publicCustomerId); log.info("Public Monitoring dashboard link: {}", link); } else { log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); From 63cc15091c7257cc8bdcf2dc9c16b4499f756709 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 2 Dec 2025 13:49:32 +0200 Subject: [PATCH 011/147] Added support for dynamic y axis limits determination --- .../chart/bar-chart-basic-config.component.ts | 30 +++ ...rt-with-labels-basic-config.component.html | 3 + .../latest-chart-basic-config.component.html | 78 ++++-- ...polar-area-chart-basic-config.component.ts | 30 +++ .../range-chart-basic-config.component.html | 3 + ...e-series-chart-basic-config.component.html | 3 + .../lib/chart/bar-chart-widget.models.ts | 5 +- .../widget/lib/chart/bars-chart.models.ts | 5 +- .../components/widget/lib/chart/bars-chart.ts | 225 +++++++++++++++++- .../lib/chart/time-series-chart.models.ts | 42 +++- .../widget/lib/chart/time-series-chart.ts | 201 ++++++++++++++++ .../bar-chart-widget-settings.component.ts | 30 +++ ...with-labels-widget-settings.component.html | 3 + ...atest-chart-widget-settings.component.html | 80 +++++-- .../latest-chart-widget-settings.component.ts | 10 + ...ar-area-chart-widget-settings.component.ts | 33 ++- ...range-chart-widget-settings.component.html | 3 + .../common/axis-scale-row.component.html | 71 ++++++ .../common/axis-scale-row.component.scss | 15 ++ .../common/axis-scale-row.component.ts | 204 ++++++++++++++++ ...s-chart-axis-settings-panel.component.html | 3 + ...ies-chart-axis-settings-panel.component.ts | 12 + ...-series-chart-axis-settings.component.html | 48 ++-- ...me-series-chart-axis-settings.component.ts | 41 +++- ...ime-series-chart-y-axes-panel.component.ts | 12 + .../time-series-chart-y-axis-row.component.ts | 7 +- .../common/widget-settings-common.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 8 + ui-ngx/tailwind.config.js | 3 +- 29 files changed, 1136 insertions(+), 79 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index ba9c41893e..9f036098be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -29,6 +29,8 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-basic-config', @@ -77,4 +79,32 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen this.widgetConfig.config.settings.axisTickLabelFont = config.axisTickLabelFont; this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; } + + protected onConfigSet(configData: WidgetConfigComponentData) { + configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); + configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); + super.onConfigSet(configData); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index 307645822d..f100ba8c88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -173,6 +173,9 @@
widgets.time-series-chart.axis.y-axis
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 31d0e6cd7e..a1b2770085 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 @@ -346,18 +346,8 @@
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
@@ -386,18 +403,8 @@
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index e0dc692e3d..c38f9e9143 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -29,6 +29,8 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-basic-config', @@ -79,4 +81,32 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; this.widgetConfig.config.settings.angleAxisStartAngle = config.angleAxisStartAngle; } + + protected onConfigSet(configData: WidgetConfigComponentData) { + configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); + configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); + super.onConfigSet(configData); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index 608284996e..174df4c692 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -235,6 +235,9 @@
widgets.time-series-chart.axis.y-axis
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 0c5eb426b3..dd535dfd4d 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 @@ -103,6 +103,9 @@ formControlName="states"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts index 3f76eb18cf..0fd3b027ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts @@ -31,10 +31,11 @@ import { ChartBarSettings, chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarChartWidgetSettings extends LatestChartWidgetSettings { - axisMin?: number; - axisMax?: number; + axisMin?: number | string | AxisLimitConfig; + axisMax?: number | string | AxisLimitConfig; axisTickLabelFont: Font; axisTickLabelColor: string; barSettings: ChartBarSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts index 2808d9f7e5..860f4da5b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts @@ -24,11 +24,12 @@ import { chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; import { Font } from '@shared/models/widget-settings.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarsChartSettings extends LatestChartSettings { polar: boolean; - axisMin?: number | string; - axisMax?: number | string; + axisMin?: number | string | AxisLimitConfig; + axisMax?: number | string | AxisLimitConfig; axisTickLabelFont: Font; axisTickLabelColor: string; angleAxisStartAngle?: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts index 92df6dd363..61adae9c20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts @@ -25,7 +25,7 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { BarDataItemOption, BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isNumber, isString } from '@core/utils'; import { ChartFillType, ChartLabelPosition, @@ -35,9 +35,19 @@ import { } from '@home/components/widget/lib/chart/chart.models'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; import { RadiusAxisOption, YAXisOption } from 'echarts/types/dist/shared'; +import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export class TbBarsChart extends TbLatestChart { + private dynamicAxisMin: number | string = null; + private dynamicAxisMax: number | string = null; + private minLatestDataKey: DataKey = null; + private maxLatestDataKey: DataKey = null; + constructor(ctx: WidgetContext, inputSettings: DeepPartial, chartElement: HTMLElement, @@ -52,6 +62,212 @@ export class TbBarsChart extends TbLatestChart { return barsChartDefaultSettings; } + protected initSettings() { + super.initSettings(); + this.setupAxisLimits(); + } + + private setupAxisLimits(): void { + const axisLimitDatasources: Datasource[] = []; + + if (isDefinedAndNotNull(this.settings.axisMin)) { + this.processAxisLimit(this.settings.axisMin, 'min', axisLimitDatasources); + } + if (isDefinedAndNotNull(this.settings.axisMax)) { + this.processAxisLimit(this.settings.axisMax, 'max', axisLimitDatasources); + } + + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processAxisLimit( + limit: any, + limitType: 'min' | 'max', + axisLimitDatasources: Datasource[] + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + const axisLimit = limit as AxisLimitConfig; + + if (axisLimit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || + (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && + d.type === (axisLimit.value as DataKey).type)); + if (latestDataKey) { + break; + } + } + } + + if (latestDataKey) { + if (limitType === 'min') { + this.minLatestDataKey = latestDataKey; + } else { + this.maxLatestDataKey = latestDataKey; + } + } + } else if (axisLimit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: (axisLimit.value as DataKey).type, + name: (axisLimit.value as DataKey).name, + label: (axisLimit.value as DataKey).name, + settings: { + axisLimit: limitType + } + }; + + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: axisLimit.entityAlias, + aliasName: axisLimit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (axisLimit.type === ValueSourceType.constant) { + const value = axisLimit.value as number; + if (limitType === 'min') { + this.dynamicAxisMin = value; + } else { + this.dynamicAxisMax = value; + } + } + } else if (typeof limit === 'number' || typeof limit === 'string') { + if (limitType === 'min') { + this.dynamicAxisMin = limit; + } else { + this.dynamicAxisMax = limit; + } + } + } + + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const data of subscription.data) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (limitType === 'min') { + if (this.dynamicAxisMin !== value) { + this.dynamicAxisMin = value; + update = true; + } + } else { + if (this.dynamicAxisMax !== value) { + this.dynamicAxisMax = value; + update = true; + } + } + } + } + } + if (this.latestChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + + private parseAxisLimitData(data: any): number | null { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + return value; + } + return null; + } + + private updateAxisLimitsFromLatest(): boolean { + let update = false; + + if (this.ctx.latestData) { + if (this.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === this.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (this.dynamicAxisMin !== value) { + this.dynamicAxisMin = value; + update = true; + } + } + } + + if (this.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === this.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1]); + if (this.dynamicAxisMax !== value) { + this.dynamicAxisMax = value; + update = true; + } + } + } + } + return update; + } + + private updateAxisLimits(): void { + if (this.latestChart && !this.latestChart.isDisposed()) { + const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, + this.settings.axisTickLabelColor, false, 'axis.tickLabel'); + const valueAxis: ValueAxisBaseOption = { + type: 'value', + min: this.dynamicAxisMin, + max: this.dynamicAxisMax, + axisLabel: { + color: axisTickLabelStyle.color, + fontStyle: axisTickLabelStyle.fontStyle, + fontWeight: axisTickLabelStyle.fontWeight, + fontFamily: axisTickLabelStyle.fontFamily, + fontSize: axisTickLabelStyle.fontSize, + formatter: (value: any) => this.valueFormatter.format(value) + } + }; + + if (this.settings.polar) { + this.latestChartOption.radiusAxis = valueAxis as RadiusAxisOption; + } else { + this.latestChartOption.yAxis = valueAxis as YAXisOption; + } + + this.latestChart.setOption(this.latestChartOption); + } + } + + public latestUpdated() { + if (this.updateAxisLimitsFromLatest()) { + this.updateAxisLimits(); + } + } + protected prepareLatestChartOption() { let labelStyle: ComponentStyle = {}; if (this.settings.barSettings.showLabel) { @@ -89,10 +305,12 @@ export class TbBarsChart extends TbLatestChart { const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, this.settings.axisTickLabelColor, false, 'axis.tickLabel'); + const minValue = isDefinedAndNotNull(this.dynamicAxisMin) ? this.dynamicAxisMin : undefined; + const maxValue = isDefinedAndNotNull(this.dynamicAxisMax) ? this.dynamicAxisMax : undefined; const valueAxis: ValueAxisBaseOption = { type: 'value', - min: this.settings.axisMin, - max: this.settings.axisMax, + min: minValue, + max: maxValue, axisLabel: { color: axisTickLabelStyle.color, fontStyle: axisTickLabelStyle.fontStyle, @@ -102,6 +320,7 @@ export class TbBarsChart extends TbLatestChart { formatter: (value: any) => this.valueFormatter.format(value) } }; + if (this.settings.polar) { this.latestChartOption.polar = { radius: '100%' diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 089c655224..b3924154d6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,12 +381,18 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string; - max?: number | string; + min?: number | string | AxisLimitConfig; + max?: number | string | AxisLimitConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } +export interface AxisLimitConfig { + entityAlias: string; + type: ValueSourceType; + value: number | DataKey; +} + export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => !(!axis.id || isUndefinedOrNull(axis.order)); @@ -867,6 +873,11 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; + dynamicMin?: string| number; + dynamicMax?: string| number; + minLatestDataKey?: DataKey; + maxLatestDataKey?: DataKey; + unitConvertor?: (value: number) => number; } export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { @@ -926,6 +937,29 @@ export const createTimeSeriesYAxis = (units: string, return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); }; } + + let initialMin: number | string | undefined; + if (isDefinedAndNotNull(settings.min)) { + if (typeof settings.min === 'object' && 'type' in settings.min) { + initialMin = undefined; + } else if (typeof settings.min === 'number') { + initialMin = unitConvertor ? unitConvertor(settings.min) : settings.min; + } else if (typeof settings.min === 'string') { + initialMin = settings.min; + } + } + + let initialMax: number | string | undefined; + if (isDefinedAndNotNull(settings.max)) { + if (typeof settings.max === 'object' && 'type' in settings.max) { + initialMax = undefined; + } else if (typeof settings.max === 'number') { + initialMax = unitConvertor ? unitConvertor(settings.max) : settings.max; + } else if (typeof settings.max === 'string') { + initialMax = settings.max; + } + } + return { id: settings.id, decimals, @@ -939,8 +973,8 @@ export const createTimeSeriesYAxis = (units: string, offset: 0, alignTicks: true, scale: true, - min: isDefinedAndNotNull(settings.min) ? unitConvertor(Number(settings.min)) : settings.min, - max: isDefinedAndNotNull(settings.max) ? unitConvertor(Number(settings.max)) : settings.max, + min: initialMin, + max: initialMax, minInterval, splitNumber, interval, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index d447b51b37..7954352bb4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -17,6 +17,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { adjustTimeAxisExtentToData, + AxisLimitConfig, calculateThresholdsOffset, createTimeSeriesVisualMapOption, createTimeSeriesXAxis, @@ -61,6 +62,8 @@ import { isDefined, isDefinedAndNotNull, isEqual, + isNumber, + isString, mergeDeep } from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; @@ -287,8 +290,14 @@ export class TbTimeSeriesChart { } } } + if (this.updateAxisLimitsFromLatest()) { + update = true; + } if (this.timeSeriesChart && update) { this.updateSeriesData(); + if (this.updateAxisLimitsFromLatest()) { + this.updateAxisLimits(); + } } } @@ -550,6 +559,7 @@ export class TbTimeSeriesChart { private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); @@ -563,8 +573,98 @@ export class TbTimeSeriesChart { axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); + if (isDefinedAndNotNull(axisSettings.min)) { + this.processAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + } + if (isDefinedAndNotNull(axisSettings.max)) { + this.processAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); + } + if (yAxis.minLatestDataKey || yAxis.maxLatestDataKey) { + yAxis.unitConvertor = unitConvertor; + } this.yAxisList.push(yAxis); } + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processAxisLimit( + limit: any, + limitType: 'min' | 'max', + yAxis: TimeSeriesChartYAxis, + axisLimitDatasources: Datasource[], + unitConvertor?: (value: number) => number + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + const axisLimit = limit as AxisLimitConfig; + if (axisLimit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || + (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && + d.type === (axisLimit.value as DataKey).type)); + if (latestDataKey) { + break; + } + } + } + if (latestDataKey) { + if (limitType === 'min') { + yAxis.minLatestDataKey = latestDataKey; + } else { + yAxis.maxLatestDataKey = latestDataKey; + } + } + } else if (axisLimit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: (axisLimit.value as DataKey).type, + name: (axisLimit.value as DataKey).name, + label: (axisLimit.value as DataKey).name, + settings: { + yAxisId: yAxis.id, + axisLimit: limitType + } + }; + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: axisLimit.entityAlias, + aliasName: axisLimit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (axisLimit.type === ValueSourceType.constant) { + const value = unitConvertor ? unitConvertor(axisLimit.value as number) : axisLimit.value as number; + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + } + } else if (typeof limit === 'number' || typeof limit === 'string') { + let value = limit; + if (typeof value === 'number' && unitConvertor) { + value = unitConvertor(value); + } + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + } } private setupVisualMap(): void { @@ -622,6 +722,45 @@ export class TbTimeSeriesChart { } } + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const yAxis of this.yAxisList) { + for (const data of subscription.data) { + if (data.dataKey.settings?.yAxisId === yAxis.id) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (limitType === 'min') { + yAxis.dynamicMin = value; + yAxis.option.min = value; + } else { + yAxis.dynamicMax = value; + yAxis.option.max = value; + } + update = true; + } + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + private drawChart() { echartsModule.init(); this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); @@ -714,6 +853,58 @@ export class TbTimeSeriesChart { } } + private updateAxisLimitsFromLatest(): boolean { + let update = false; + + if (this.ctx.latestData) { + for (const yAxis of this.yAxisList) { + if (yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + + if (yAxis.dynamicMin !== value) { + yAxis.dynamicMin = value; + yAxis.option.min = value; + update = true; + } + } + } + + if (yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.dynamicMax !== value) { + yAxis.dynamicMax = value; + yAxis.option.max = value; + update = true; + } + } + } + } + } + return update; + } + + private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + if (unitConvertor) { + return unitConvertor(value); + } + return value; + } + return null; + }; + private updateSeries(): void { this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.stackMode, @@ -836,6 +1027,16 @@ export class TbTimeSeriesChart { return changed; } + private updateAxisLimits(): void { + if (this.timeSeriesChart && !this.timeSeriesChart.isDisposed()) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, { + replaceMerge: ['yAxis'] + }); + this.updateAxes(); + } + } + private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { if (!this.stateData) { const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts index 8826e9afda..b1734ff45a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts @@ -26,6 +26,8 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-widget-settings', @@ -57,4 +59,32 @@ export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsCo latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [])); } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + settings.axisMin = this.normalizeAxisLimit(settings.axisMin); + settings.axisMax = this.normalizeAxisLimit(settings.axisMax); + return super.prepareInputSettings(settings); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 488a08e161..08dd087cdb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -91,6 +91,9 @@
widgets.time-series-chart.axis.y-axis
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+
+ + + + + +
+
+
+
@@ -311,18 +330,8 @@
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-appearance
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - -
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
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 ff7641bf1a..fadee36370 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 @@ -22,6 +22,7 @@ import { LatestChartWidgetSettings } from '@home/components/widget/lib/chart/latest-chart.models'; import { + Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, @@ -103,6 +104,15 @@ export abstract class LatestChartWidgetSettingsComponent, protected fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 07d43f4de2..61c533a377 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,6 +26,8 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -52,10 +54,37 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett protected setupLatestChartControls(latestChartWidgetSettingsForm: UntypedFormGroup, settings: WidgetSettings) { latestChartWidgetSettingsForm.addControl('barSettings', this.fb.control(settings.barSettings, [])); - latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin, [])); - latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax, [])); + latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin)); + latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax)); latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [Validators.min(0)])); latestChartWidgetSettingsForm.addControl('angleAxisStartAngle', this.fb.control(settings.angleAxisStartAngle, [])); } + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + settings.axisMin = this.normalizeAxisLimit(settings.axisMin); + settings.axisMax = this.normalizeAxisLimit(settings.axisMax); + return super.prepareInputSettings(settings); + } + + private normalizeAxisLimit(limit: any): AxisLimitConfig { + if (limit && typeof limit === 'object' && 'type' in limit) { + return { + type: limit.type || ValueSourceType.constant, + value: limit.value ?? null, + entityAlias: limit.entityAlias ?? null + }; + } + if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html index f19784735c..c27b7c46b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html @@ -169,6 +169,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html new file mode 100644 index 0000000000..eed985187d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -0,0 +1,71 @@ + +
+
+ {{ labelKey | translate}} +
+
+ + + + {{ ValueSourceTypeTranslation.get(type) | translate }} + + + + + +
+
+ + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss new file mode 100644 index 0000000000..1a4214be18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts new file mode 100644 index 0000000000..b8103dd008 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, Validator, + Validators, +} from '@angular/forms'; +import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { Datasource, DatasourceType, } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-axis-scale-row', + templateUrl: './axis-scale-row.component.html', + styleUrl: './axis-scale-row.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AxisScaleRowComponent), + multi: true + }, + ] +}) +export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + isPanelView = false; + + @Input() + aliasController: IAliasController; + + @Input() + callbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + labelKey: string; + + ValueSourceType = ValueSourceType; + + DataKeyType = DataKeyType; + + DatasourceType = DatasourceType; + + ValueSourceTypeTranslation = ValueSourceTypeTranslation; + + ValueSourceTypes = ValueSourceTypes; + + limitForm: UntypedFormGroup; + + private propagateChanges: (value: any) => void = () => {}; + + private modelValue: AxisLimitConfig | null = null; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.limitForm = this.fb.group({ + type: [ValueSourceType.constant], + value: [null], + entityAlias: [null] + }); + this.subscribeToTypeChanges(); + + this.limitForm.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.updateValidators(); + this.updateView(value); + }); + } + + writeValue(value: AxisLimitConfig) { + this.modelValue = value; + + if (!this.limitForm) { + return; + } + + if (value) { + this.limitForm.patchValue({ + type: value.type ?? ValueSourceType.constant, + value: value.value ?? null, + entityAlias: value.entityAlias ?? null + }, + { emitEvent: false } + ); + } else { + this.limitForm.patchValue({ + type: ValueSourceType.constant, + value: null, + entityAlias: null + }, + { emitEvent: false } + ); + } + + this.updateValidators(); + } + + registerOnChange(fn: any) { + this.propagateChanges = fn; + } + + registerOnTouched(fn: any) { + } + + validate(): ValidationErrors | null { + return this.limitForm.valid ? null : { + axisLimitForm: { + valid: false, + errors: this.getFormErrors() + } + }; + } + + private getFormErrors(): any { + const errors: any = {}; + Object.keys(this.limitForm.controls).forEach(key => { + const control = this.limitForm.get(key); + if (control && control.errors) { + errors[key] = control.errors; + } + }); + return errors; + } + + private updateValidators() { + const axisTypeControl = this.limitForm.get('type'); + const axisValueControl = this.limitForm.get('value'); + const axisEntityAliasControl = this.limitForm.get('entityAlias'); + + if (axisTypeControl && axisValueControl && axisEntityAliasControl) { + const type = axisTypeControl.value; + if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) { + axisValueControl.setValidators([Validators.required]); + } else { + axisValueControl.clearValidators(); + } + + if (type === ValueSourceType.entity) { + axisEntityAliasControl.setValidators([Validators.required]); + } else { + axisEntityAliasControl.clearValidators(); + } + + axisValueControl.updateValueAndValidity({ emitEvent: false }); + axisEntityAliasControl.updateValueAndValidity({ emitEvent: false }); + } + } + + private subscribeToTypeChanges() { + this.limitForm.controls.type.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + const axisValueControl = this.limitForm.get('value'); + const axisEntityAliasControl = this.limitForm.get('entityAlias'); + if (axisValueControl) { + axisValueControl.setValue(null, { emitEvent: true }); + } + if (axisEntityAliasControl) { + axisEntityAliasControl.setValue(null, { emitEvent: true }); + } + }); + } + + private updateView(value: AxisLimitConfig) { + if (!isEqual(this.modelValue, value)) { + this.modelValue = value; + this.propagateChanges(value); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index a462c92c03..bd6e8e888a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -19,6 +19,9 @@
{{ panelTitle }}
+
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
+
-
-
-
widgets.chart.chart-axis.scale
-
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - - -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 40f2a96c69..4bf7a78f98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -17,9 +17,11 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup, + ValidationErrors, Validator, Validators } from '@angular/forms'; import { @@ -32,6 +34,9 @@ import { merge } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { Datasource } from '@app/shared/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; @Component({ selector: 'tb-time-series-chart-axis-settings', @@ -42,10 +47,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true } ] }) -export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { +export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator { @Input() @coerceBoolean() @@ -61,6 +71,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu defaultXAxisTicksFormat = defaultXAxisTicksFormat; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -147,6 +166,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu registerOnTouched(_fn: any): void { } + validate(): ValidationErrors | null { + return this.axisSettingsFormGroup.valid ? null : { + axisSettings: { + valid: false, + errors: this.getFormErrors() + } + }; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { @@ -222,6 +250,17 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu } } + private getFormErrors(): any { + const errors: any = {}; + Object.keys(this.axisSettingsFormGroup.controls).forEach(key => { + const control = this.axisSettingsFormGroup.get(key); + if (control && control.errors) { + errors[key] = control.errors; + } + }); + return errors; + } + private updateModel() { this.modelValue = this.axisSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 12ac24f9e3..4af0f5f613 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -47,6 +47,9 @@ import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { Datasource } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -68,6 +71,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index ebc24ddd22..1857852afe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -42,6 +42,7 @@ import { import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -85,6 +86,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private popoverService: TbPopoverService, + private timeSeriesChartYAxesPanel: TimeSeriesChartYAxesPanelComponent, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, @@ -165,7 +167,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O axisType: 'yAxis', panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), axisSettings: deepClone(this.modelValue), - advanced: this.advanced + advanced: this.advanced, + aliasController: this.timeSeriesChartYAxesPanel.aliasController, + dataKeyCallbacks: this.timeSeriesChartYAxesPanel.dataKeyCallbacks, + datasource: this.timeSeriesChartYAxesPanel.datasource }, isModal: true }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 233b202684..385bc55495 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -267,6 +267,7 @@ import { import { ShapeFillStripeSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; +import { AxisScaleRowComponent } from './axis-scale-row.component'; @NgModule({ declarations: [ @@ -372,7 +373,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], imports: [ CommonModule, @@ -453,7 +455,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], providers: [ ColorSettingsComponentService, 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 061ce65f0e..25eaa16793 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7638,6 +7638,14 @@ "update-animation-delay": "Update animation delay" }, "chart-axis": { + "limit":"Limit", + "source": "Source", + "key-value": "Key / Value", + "value-required": "Value is required.", + "entity-key-required": "Entity key is required.", + "key-required": "Key is required.", + "scale-limits": "Scale limits", + "scale-appearance": "Scale appearance", "scale": "Scale", "scale-min": "min", "scale-max": "max", diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index d675da4cda..bc516ddf40 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -84,7 +84,8 @@ module.exports = { '25': '6.25rem', '37.5': '9.375rem', '62.5': '15.625rem', - '72.5': '18.125rem' + '72.5': '18.125rem', + '40%': '40%' }, flex: { full: '1 1 100%' From b6c21e9748f5a6023092d036b7c421cf2779beb7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Dec 2025 13:04:33 +0200 Subject: [PATCH 012/147] improved env descriptions for site --- .../src/main/resources/thingsboard.yml | 38 +++++++++---------- .../src/main/resources/tb-coap-transport.yml | 14 +++---- .../src/main/resources/tb-lwm2m-transport.yml | 16 ++++---- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d3d04bcea0..f7b23f9360 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -738,13 +738,13 @@ redis: # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" sentinel: - # name of the master node + # Name of the master node master: "${REDIS_MASTER:}" - # comma-separated list of "host:port" pairs of sentinels + # Comma-separated list of "host:port" pairs of sentinels sentinels: "${REDIS_SENTINELS:}" - # password to authenticate with sentinel + # Password to authenticate with sentinel password: "${REDIS_SENTINEL_PASSWORD:}" - # if set false will be used pool config build from values of the pool config section + # If set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" @@ -1165,15 +1165,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID @@ -1356,14 +1356,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 2f3942f847..497af7b322 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -185,14 +185,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 323f80b999..51c93948a5 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -164,15 +164,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID From ba6d84c40bebe4b16cb2381420de506654d7d61d Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 3 Dec 2025 17:32:21 +0200 Subject: [PATCH 013/147] fixed latest limit type display --- .../chart/bar-chart-basic-config.component.ts | 32 +-- .../latest-chart-basic-config.component.html | 132 ++++------ ...polar-area-chart-basic-config.component.ts | 32 +-- .../lib/chart/bar-chart-widget.models.ts | 5 +- .../widget/lib/chart/bars-chart.models.ts | 5 +- .../components/widget/lib/chart/bars-chart.ts | 225 +----------------- .../widget/lib/chart/latest-chart.ts | 1 + .../lib/chart/time-series-chart.models.ts | 12 +- .../widget/lib/chart/time-series-chart.ts | 133 +++++------ .../bar-chart-widget-settings.component.ts | 30 --- ...atest-chart-widget-settings.component.html | 94 +++----- .../latest-chart-widget-settings.component.ts | 10 - ...ar-area-chart-widget-settings.component.ts | 22 +- .../common/axis-scale-row.component.html | 4 +- .../common/axis-scale-row.component.ts | 129 +++++----- ...ime-series-chart-y-axes-panel.component.ts | 104 +++++++- 16 files changed, 328 insertions(+), 642 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index a58ddd8302..4c7da10bc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -29,8 +29,6 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-basic-config', @@ -43,7 +41,7 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen barChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { @@ -81,32 +79,4 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen this.widgetConfig.config.settings.axisTickLabelFont = config.axisTickLabelFont; this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; } - - protected onConfigSet(configData: WidgetConfigComponentData) { - configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); - configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); - super.onConfigSet(configData); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } 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 27c173bd17..d72a8d6083 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 @@ -21,27 +21,27 @@ formControlName="timewindowConfig"> + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources"> + panelTitle="{{ 'widgets.chart.series' | translate }}" + addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" + keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" + removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" + noKeysText="{{ 'widgets.chart.no-series' | translate }}" + requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" + hideUnits + hideDecimals + hideDataKeyUnits + hideDataKeyDecimals + [datasourceType]="datasource?.type" + [deviceId]="datasource?.deviceId" + [entityAliasId]="datasource?.entityAliasId" + formControlName="series">
widget-config.appearance
@@ -210,7 +210,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -239,7 +239,7 @@
+ formControlName="actions"> @@ -346,15 +346,25 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -402,16 +385,26 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -542,7 +508,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index d29af73b75..f60f8d2417 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -29,8 +29,6 @@ import { import { LatestChartBasicConfigComponent } from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-basic-config', @@ -43,7 +41,7 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo polarAreaChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { @@ -83,32 +81,4 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor; this.widgetConfig.config.settings.angleAxisStartAngle = config.angleAxisStartAngle; } - - protected onConfigSet(configData: WidgetConfigComponentData) { - configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin); - configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax); - super.onConfigSet(configData); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts index 0fd3b027ab..3f76eb18cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts @@ -31,11 +31,10 @@ import { ChartBarSettings, chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarChartWidgetSettings extends LatestChartWidgetSettings { - axisMin?: number | string | AxisLimitConfig; - axisMax?: number | string | AxisLimitConfig; + axisMin?: number; + axisMax?: number; axisTickLabelFont: Font; axisTickLabelColor: string; barSettings: ChartBarSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts index 860f4da5b9..2808d9f7e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts @@ -24,12 +24,11 @@ import { chartColorScheme } from '@home/components/widget/lib/chart/chart.models'; import { Font } from '@shared/models/widget-settings.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export interface BarsChartSettings extends LatestChartSettings { polar: boolean; - axisMin?: number | string | AxisLimitConfig; - axisMax?: number | string | AxisLimitConfig; + axisMin?: number | string; + axisMax?: number | string; axisTickLabelFont: Font; axisTickLabelColor: string; angleAxisStartAngle?: number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts index 61adae9c20..92df6dd363 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts @@ -25,7 +25,7 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import { BarDataItemOption, BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries'; -import { isDefinedAndNotNull, isNumber, isString } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; import { ChartFillType, ChartLabelPosition, @@ -35,19 +35,9 @@ import { } from '@home/components/widget/lib/chart/chart.models'; import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; import { RadiusAxisOption, YAXisOption } from 'echarts/types/dist/shared'; -import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; -import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; export class TbBarsChart extends TbLatestChart { - private dynamicAxisMin: number | string = null; - private dynamicAxisMax: number | string = null; - private minLatestDataKey: DataKey = null; - private maxLatestDataKey: DataKey = null; - constructor(ctx: WidgetContext, inputSettings: DeepPartial, chartElement: HTMLElement, @@ -62,212 +52,6 @@ export class TbBarsChart extends TbLatestChart { return barsChartDefaultSettings; } - protected initSettings() { - super.initSettings(); - this.setupAxisLimits(); - } - - private setupAxisLimits(): void { - const axisLimitDatasources: Datasource[] = []; - - if (isDefinedAndNotNull(this.settings.axisMin)) { - this.processAxisLimit(this.settings.axisMin, 'min', axisLimitDatasources); - } - if (isDefinedAndNotNull(this.settings.axisMax)) { - this.processAxisLimit(this.settings.axisMax, 'max', axisLimitDatasources); - } - - this.subscribeForAxisLimits(axisLimitDatasources); - } - - private processAxisLimit( - limit: any, - limitType: 'min' | 'max', - axisLimitDatasources: Datasource[] - ): void { - if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as AxisLimitConfig; - - if (axisLimit.type === ValueSourceType.latestKey) { - let latestDataKey: DataKey = null; - if (this.ctx.datasources.length) { - for (const datasource of this.ctx.datasources) { - latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || - (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && - d.type === (axisLimit.value as DataKey).type)); - if (latestDataKey) { - break; - } - } - } - - if (latestDataKey) { - if (limitType === 'min') { - this.minLatestDataKey = latestDataKey; - } else { - this.maxLatestDataKey = latestDataKey; - } - } - } else if (axisLimit.type === ValueSourceType.entity) { - const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); - if (entityAliasId) { - let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); - const entityDataKey: DataKey = { - type: (axisLimit.value as DataKey).type, - name: (axisLimit.value as DataKey).name, - label: (axisLimit.value as DataKey).name, - settings: { - axisLimit: limitType - } - }; - - if (datasource) { - datasource.dataKeys.push(entityDataKey); - } else { - datasource = { - type: DatasourceType.entity, - name: axisLimit.entityAlias, - aliasName: axisLimit.entityAlias, - entityAliasId, - dataKeys: [entityDataKey] - }; - axisLimitDatasources.push(datasource); - } - } - } else if (axisLimit.type === ValueSourceType.constant) { - const value = axisLimit.value as number; - if (limitType === 'min') { - this.dynamicAxisMin = value; - } else { - this.dynamicAxisMax = value; - } - } - } else if (typeof limit === 'number' || typeof limit === 'string') { - if (limitType === 'min') { - this.dynamicAxisMin = limit; - } else { - this.dynamicAxisMax = limit; - } - } - } - - private subscribeForAxisLimits(datasources: Datasource[]) { - if (datasources.length) { - const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { - datasources, - useDashboardTimewindow: false, - type: widgetType.latest, - callbacks: { - onDataUpdated: (subscription) => { - let update = false; - if (subscription.data) { - for (const data of subscription.data) { - const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); - if (data.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (limitType === 'min') { - if (this.dynamicAxisMin !== value) { - this.dynamicAxisMin = value; - update = true; - } - } else { - if (this.dynamicAxisMax !== value) { - this.dynamicAxisMax = value; - update = true; - } - } - } - } - } - if (this.latestChart && update) { - this.updateAxisLimits(); - } - } - } - }; - this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); - } - } - - private parseAxisLimitData(data: any): number | null { - let value: number; - if (isDefinedAndNotNull(data)) { - if (isNumber(data)) { - value = data; - } else if (isString(data)) { - value = Number(data); - } - } - if (isDefinedAndNotNull(value) && !isNaN(value)) { - return value; - } - return null; - } - - private updateAxisLimitsFromLatest(): boolean { - let update = false; - - if (this.ctx.latestData) { - if (this.minLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === this.minLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (this.dynamicAxisMin !== value) { - this.dynamicAxisMin = value; - update = true; - } - } - } - - if (this.maxLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === this.maxLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1]); - if (this.dynamicAxisMax !== value) { - this.dynamicAxisMax = value; - update = true; - } - } - } - } - return update; - } - - private updateAxisLimits(): void { - if (this.latestChart && !this.latestChart.isDisposed()) { - const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, - this.settings.axisTickLabelColor, false, 'axis.tickLabel'); - const valueAxis: ValueAxisBaseOption = { - type: 'value', - min: this.dynamicAxisMin, - max: this.dynamicAxisMax, - axisLabel: { - color: axisTickLabelStyle.color, - fontStyle: axisTickLabelStyle.fontStyle, - fontWeight: axisTickLabelStyle.fontWeight, - fontFamily: axisTickLabelStyle.fontFamily, - fontSize: axisTickLabelStyle.fontSize, - formatter: (value: any) => this.valueFormatter.format(value) - } - }; - - if (this.settings.polar) { - this.latestChartOption.radiusAxis = valueAxis as RadiusAxisOption; - } else { - this.latestChartOption.yAxis = valueAxis as YAXisOption; - } - - this.latestChart.setOption(this.latestChartOption); - } - } - - public latestUpdated() { - if (this.updateAxisLimitsFromLatest()) { - this.updateAxisLimits(); - } - } - protected prepareLatestChartOption() { let labelStyle: ComponentStyle = {}; if (this.settings.barSettings.showLabel) { @@ -305,12 +89,10 @@ export class TbBarsChart extends TbLatestChart { const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont, this.settings.axisTickLabelColor, false, 'axis.tickLabel'); - const minValue = isDefinedAndNotNull(this.dynamicAxisMin) ? this.dynamicAxisMin : undefined; - const maxValue = isDefinedAndNotNull(this.dynamicAxisMax) ? this.dynamicAxisMax : undefined; const valueAxis: ValueAxisBaseOption = { type: 'value', - min: minValue, - max: maxValue, + min: this.settings.axisMin, + max: this.settings.axisMax, axisLabel: { color: axisTickLabelStyle.color, fontStyle: axisTickLabelStyle.fontStyle, @@ -320,7 +102,6 @@ export class TbBarsChart extends TbLatestChart { formatter: (value: any) => this.valueFormatter.format(value) } }; - if (this.settings.polar) { this.latestChartOption.polar = { radius: '100%' diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index 9e525cd794..5911a60215 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -142,6 +142,7 @@ export abstract class TbLatestChart { public update(): void { for (const dsData of this.ctx.data) { + console.log("this.ctx.data",this.ctx.data) let value = 0; const tsValue = dsData.data[0]; const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index e5987e9664..5585496ede 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,18 +381,12 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string | AxisLimitConfig; - max?: number | string | AxisLimitConfig; + min?: string | ValueSourceConfig; + max?: string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } -export interface AxisLimitConfig { - entityAlias: string; - type: ValueSourceType; - value: number | DataKey; -} - export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => !(!axis.id || isUndefinedOrNull(axis.order)); @@ -873,8 +867,6 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; - dynamicMin?: string| number; - dynamicMax?: string| number; minLatestDataKey?: DataKey; maxLatestDataKey?: DataKey; unitConvertor?: (value: number) => number; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 7954352bb4..d429438f43 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -17,7 +17,6 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { adjustTimeAxisExtentToData, - AxisLimitConfig, calculateThresholdsOffset, createTimeSeriesVisualMapOption, createTimeSeriesXAxis, @@ -55,7 +54,7 @@ import { getFocusedSeriesIndex, measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; +import { DateFormatProcessor, ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; import { formattedDataFormDatasourceData, formatValue, @@ -289,15 +288,36 @@ export class TbTimeSeriesChart { } } } - } - if (this.updateAxisLimitsFromLatest()) { - update = true; + + for (const yAxis of this.yAxisList) { + const minType = (yAxis.settings.min as ValueSourceConfig).type; + const maxType = (yAxis.settings.max as ValueSourceConfig).type; + if (minType === ValueSourceType.latestKey && yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } + } + + if (maxType === ValueSourceType.latestKey && yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } } if (this.timeSeriesChart && update) { this.updateSeriesData(); - if (this.updateAxisLimitsFromLatest()) { - this.updateAxisLimits(); - } + this.updateAxisLimits(); } } @@ -574,20 +594,17 @@ export class TbTimeSeriesChart { } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); if (isDefinedAndNotNull(axisSettings.min)) { - this.processAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); } if (isDefinedAndNotNull(axisSettings.max)) { - this.processAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); - } - if (yAxis.minLatestDataKey || yAxis.maxLatestDataKey) { - yAxis.unitConvertor = unitConvertor; + this.processYAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); } this.yAxisList.push(yAxis); } this.subscribeForAxisLimits(axisLimitDatasources); } - private processAxisLimit( + private processYAxisLimit( limit: any, limitType: 'min' | 'max', yAxis: TimeSeriesChartYAxis, @@ -595,15 +612,15 @@ export class TbTimeSeriesChart { unitConvertor?: (value: number) => number ): void { if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as AxisLimitConfig; + const axisLimit = limit as ValueSourceConfig; if (axisLimit.type === ValueSourceType.latestKey) { let latestDataKey: DataKey = null; if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === (axisLimit.value as DataKey).label) || - (d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name && - d.type === (axisLimit.value as DataKey).type)); + (d.type === DataKeyType.function && d.label === axisLimit.latestKey) || + (d.type !== DataKeyType.function && d.name === axisLimit.latestKey && + d.type === axisLimit.latestKeyType)); if (latestDataKey) { break; } @@ -621,9 +638,9 @@ export class TbTimeSeriesChart { if (entityAliasId) { let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); const entityDataKey: DataKey = { - type: (axisLimit.value as DataKey).type, - name: (axisLimit.value as DataKey).name, - label: (axisLimit.value as DataKey).name, + type: limit.entityKeyType, + name: limit.entityKey, + label: limit.entityKey, settings: { yAxisId: yAxis.id, axisLimit: limitType @@ -643,27 +660,14 @@ export class TbTimeSeriesChart { } } } else if (axisLimit.type === ValueSourceType.constant) { - const value = unitConvertor ? unitConvertor(axisLimit.value as number) : axisLimit.value as number; + const value = unitConvertor ? unitConvertor(axisLimit.value) : axisLimit.value; if (limitType === 'min') { - yAxis.dynamicMin = value; yAxis.option.min = value; } else { - yAxis.dynamicMax = value; yAxis.option.max = value; } } - } else if (typeof limit === 'number' || typeof limit === 'string') { - let value = limit; - if (typeof value === 'number' && unitConvertor) { - value = unitConvertor(value); - } - if (limitType === 'min') { - yAxis.dynamicMin = value; - yAxis.option.min = value; - } else { - yAxis.dynamicMax = value; - yAxis.option.max = value; - } + return; } } @@ -738,25 +742,30 @@ export class TbTimeSeriesChart { const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); if (data.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (isDefinedAndNotNull(value)) { if (limitType === 'min') { - yAxis.dynamicMin = value; - yAxis.option.min = value; + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } } else { - yAxis.dynamicMax = value; - yAxis.option.max = value; + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } } - update = true; } } } } } - if (this.timeSeriesChart && update) { - this.updateAxisLimits(); - } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); } } - }; + } + }; this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); } } @@ -853,40 +862,6 @@ export class TbTimeSeriesChart { } } - private updateAxisLimitsFromLatest(): boolean { - let update = false; - - if (this.ctx.latestData) { - for (const yAxis of this.yAxisList) { - if (yAxis.minLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - - if (yAxis.dynamicMin !== value) { - yAxis.dynamicMin = value; - yAxis.option.min = value; - update = true; - } - } - } - - if (yAxis.maxLatestDataKey) { - const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); - if (data?.data[0]) { - const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - if (yAxis.dynamicMax !== value) { - yAxis.dynamicMax = value; - yAxis.option.max = value; - update = true; - } - } - } - } - } - return update; - } - private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { let value: number; if (isDefinedAndNotNull(data)) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts index b1734ff45a..8826e9afda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts @@ -26,8 +26,6 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-bar-chart-widget-settings', @@ -59,32 +57,4 @@ export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsCo latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [])); } - - protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { - settings.axisMin = this.normalizeAxisLimit(settings.axisMin); - settings.axisMax = this.normalizeAxisLimit(settings.axisMax); - return super.prepareInputSettings(settings); - } - - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { - return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null - }; - } - if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } } 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 6bbb996e93..3f1e96ca84 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 @@ -120,7 +120,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -256,15 +256,25 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
widgets.bar-chart.bar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
-
- - - - - -
-
-
-
@@ -322,16 +303,26 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
widgets.polar-area-chart.polar-axis
-
widgets.chart.chart-axis.scale-appearance
+
widgets.chart.chart-axis.scale
+
widgets.chart.chart-axis.scale-min
+ + + +
widgets.chart.chart-axis.scale-max
+ + +
-
-
widgets.chart.chart-axis.scale-limits
-
-
-
widgets.chart.chart-axis.limit
-
widgets.chart.chart-axis.source
-
widgets.chart.chart-axis.key-value
-
-
- - - - - -
-
-
@@ -470,7 +434,7 @@ + formControlName="fillAreaSettings">
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 fadee36370..ff7641bf1a 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 @@ -22,7 +22,6 @@ import { LatestChartWidgetSettings } from '@home/components/widget/lib/chart/latest-chart.models'; import { - Datasource, legendPositions, legendPositionTranslationMap, WidgetSettings, @@ -104,15 +103,6 @@ export abstract class LatestChartWidgetSettingsComponent, protected fb: UntypedFormBuilder) { super(store); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 61c533a377..355dc55207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,8 +26,7 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { ValueSourceType } from '@shared/models/widget-settings.models'; +import { ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -66,25 +65,20 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett return super.prepareInputSettings(settings); } - private normalizeAxisLimit(limit: any): AxisLimitConfig { - if (limit && typeof limit === 'object' && 'type' in limit) { + private normalizeAxisLimit(limit: any): ValueSourceConfig { + if (!limit) { return { - type: limit.type || ValueSourceType.constant, - value: limit.value ?? null, - entityAlias: limit.entityAlias ?? null + type: ValueSourceType.constant, + value: null, + entityAlias: null }; - } - if (typeof limit === 'number') { + } else if (typeof limit === 'number') { return { type: ValueSourceType.constant, value: limit, entityAlias: null }; } - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; + return limit; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html index eed985187d..37c2838a21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -53,7 +53,7 @@ [dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" [callbacks]="callbacks" [editable]="false" - formControlName="value"> + [formControl]="latestKeyFormControl"> + [formControl]="entityKeyFormControl">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index b8103dd008..165857eac3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -18,18 +18,22 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core' import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormBuilder, + UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validator, Validators, } from '@angular/forms'; -import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models'; +import { + ValueSourceConfig, + ValueSourceType, + ValueSourceTypes, + ValueSourceTypeTranslation +} from '@shared/models/widget-settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; -import { Datasource, DatasourceType, } from '@shared/models/widget.models'; +import { DataKey, Datasource, DatasourceType, } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { IAliasController } from '@core/api/widget-api.models'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { isEqual } from '@core/utils'; +import { merge } from 'rxjs'; @Component({ selector: 'tb-axis-scale-row', @@ -77,9 +81,13 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali limitForm: UntypedFormGroup; + latestKeyFormControl: UntypedFormControl; + + entityKeyFormControl: UntypedFormControl; + private propagateChanges: (value: any) => void = () => {}; - private modelValue: AxisLimitConfig | null = null; + private modelValue: ValueSourceConfig | null = null; constructor(private fb: UntypedFormBuilder, private destroyRef: DestroyRef) { @@ -91,42 +99,44 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali value: [null], entityAlias: [null] }); + this.latestKeyFormControl = this.fb.control(null, [Validators.required]); + this.entityKeyFormControl = this.fb.control(null, [Validators.required]); this.subscribeToTypeChanges(); - - this.limitForm.valueChanges - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(value => { + merge( + this.latestKeyFormControl.valueChanges, + this.entityKeyFormControl.valueChanges, + this.limitForm.valueChanges + ).pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { this.updateValidators(); - this.updateView(value); + this.updateModel(); }); } - writeValue(value: AxisLimitConfig) { + writeValue(value: ValueSourceConfig) { + console.log("value", value); this.modelValue = value; - - if (!this.limitForm) { - return; - } - - if (value) { - this.limitForm.patchValue({ - type: value.type ?? ValueSourceType.constant, - value: value.value ?? null, - entityAlias: value.entityAlias ?? null - }, - { emitEvent: false } - ); - } else { - this.limitForm.patchValue({ - type: ValueSourceType.constant, - value: null, - entityAlias: null - }, - { emitEvent: false } - ); + this.limitForm.patchValue( + { + type: value.type || ValueSourceType.constant, + value: value.value, + entityAlias: value.entityAlias, + }, {emitEvent: false} + ); + if (value.type === ValueSourceType.latestKey) { + this.latestKeyFormControl.patchValue({ + type: value.latestKeyType, + name: value.latestKey + }, {emitEvent: false}); + } else if (value.type === ValueSourceType.entity) { + this.entityKeyFormControl.patchValue({ + type: value.entityKeyType, + name: value.entityKey + }, {emitEvent: false}); } this.updateValidators(); + this.limitForm.markAllAsTouched(); } registerOnChange(fn: any) { @@ -158,28 +168,22 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali private updateValidators() { const axisTypeControl = this.limitForm.get('type'); - const axisValueControl = this.limitForm.get('value'); - const axisEntityAliasControl = this.limitForm.get('entityAlias'); - - if (axisTypeControl && axisValueControl && axisEntityAliasControl) { + if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { const type = axisTypeControl.value; - if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) { - axisValueControl.setValidators([Validators.required]); - } else { - axisValueControl.clearValidators(); - } - - if (type === ValueSourceType.entity) { - axisEntityAliasControl.setValidators([Validators.required]); + if (type === ValueSourceType.latestKey) { + this.latestKeyFormControl.setValidators([Validators.required]); + this.entityKeyFormControl.clearValidators(); + } else if (type === ValueSourceType.entity) { + this.latestKeyFormControl.clearValidators(); + this.entityKeyFormControl.setValidators([Validators.required]); } else { - axisEntityAliasControl.clearValidators(); + this.latestKeyFormControl.clearValidators(); + this.entityKeyFormControl.clearValidators(); } - - axisValueControl.updateValueAndValidity({ emitEvent: false }); - axisEntityAliasControl.updateValueAndValidity({ emitEvent: false }); + this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); + this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); } } - private subscribeToTypeChanges() { this.limitForm.controls.type.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) @@ -187,18 +191,31 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali const axisValueControl = this.limitForm.get('value'); const axisEntityAliasControl = this.limitForm.get('entityAlias'); if (axisValueControl) { - axisValueControl.setValue(null, { emitEvent: true }); + axisValueControl.setValue(null, { emitEvent: false }); } if (axisEntityAliasControl) { - axisEntityAliasControl.setValue(null, { emitEvent: true }); + axisEntityAliasControl.setValue(null, { emitEvent: false }); } + this.latestKeyFormControl.setValue(null, { emitEvent: false }); + this.entityKeyFormControl.setValue(null, { emitEvent: false }); + }); } - private updateView(value: AxisLimitConfig) { - if (!isEqual(this.modelValue, value)) { - this.modelValue = value; - this.propagateChanges(value); + private updateModel() { + const value = this.limitForm.value; + this.modelValue.type = value.type ?? ValueSourceType.constant; + this.modelValue.value = value?.value; + this.modelValue.entityAlias = value?.entityAlias; + if (value.type === ValueSourceType.latestKey) { + const latestKey: DataKey = this.latestKeyFormControl.value; + this.modelValue.latestKey = latestKey?.name; + this.modelValue.latestKeyType = (latestKey?.type as any); + } else if (value.type === ValueSourceType.entity) { + const entityKey: DataKey = this.entityKeyFormControl.value; + this.modelValue.entityKey = entityKey?.name; + this.modelValue.entityKeyType = (entityKey?.type as any); } + this.propagateChanges(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 4af0f5f613..3159a27abd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -38,7 +38,8 @@ import { import { defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, - TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxes, + TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, timeSeriesChartYAxisValidator @@ -49,7 +50,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IAliasController } from '@app/core/public-api'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { Datasource } from '@app/shared/public-api'; +import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -125,6 +126,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } + this.updateLatestDataKeys(Object.values(yAxes)); this.propagateChange(yAxes); } ); @@ -147,7 +149,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = value || {}; + const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {}); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -202,8 +204,104 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { const axesControls: Array = []; axes.forEach((axis) => { + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); }); return this.fb.array(axesControls); } + + private checkLatestDataKeys(yAxes: TimeSeriesChartYAxes): TimeSeriesChartYAxes { + const latestKeys = this.datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + + const minCfg = axis.min as unknown as ValueSourceConfig; + const maxCfg = axis.max as unknown as ValueSourceConfig; + + const minValid = + minCfg?.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, minCfg)); + + const maxValid = + maxCfg?.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, maxCfg)); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; + } + + private updateLatestDataKeys(yAxes: TimeSeriesChartYAxisSettings[]) { + if (this.datasource) { + let latestKeys = this.datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + this.datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, min)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, max)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } + } + + private isYAxisKey(d: DataKey, limit: ValueSourceConfig): boolean { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); + } + + private normalizeAxisLimit(limit: any): ValueSourceConfig { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number') { + return { + type: ValueSourceType.constant, + value: limit, + entityAlias: null + }; + } + return limit; + } } From d898674268675ef8fbd85cadae69a69ff9a6bb14 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 3 Dec 2025 17:51:25 +0200 Subject: [PATCH 014/147] clean up --- .../chart/bar-chart-basic-config.component.ts | 2 +- .../latest-chart-basic-config.component.html | 54 +++++++++---------- ...polar-area-chart-basic-config.component.ts | 2 +- .../widget/lib/chart/latest-chart.ts | 1 - .../lib/chart/time-series-chart.models.ts | 4 +- ...atest-chart-widget-settings.component.html | 14 ++--- ...ar-area-chart-widget-settings.component.ts | 27 +--------- 7 files changed, 40 insertions(+), 64 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts index 4c7da10bc6..b7d6b4f640 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts @@ -41,7 +41,7 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen barChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { 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 d72a8d6083..c563820054 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 @@ -21,27 +21,27 @@ formControlName="timewindowConfig"> + [configMode]="basicMode" + hideDatasourceLabel + hideDataKeys + forceSingleDatasource + formControlName="datasources"> + panelTitle="{{ 'widgets.chart.series' | translate }}" + addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" + keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" + removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" + noKeysText="{{ 'widgets.chart.no-series' | translate }}" + requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" + hideUnits + hideDecimals + hideDataKeyUnits + hideDataKeyDecimals + [datasourceType]="datasource?.type" + [deviceId]="datasource?.deviceId" + [entityAliasId]="datasource?.entityAliasId" + formControlName="series">
widget-config.appearance
@@ -210,7 +210,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -239,7 +239,7 @@
+ formControlName="actions"> @@ -346,8 +346,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -385,9 +385,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -508,7 +508,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts index f60f8d2417..8c68ec2ff5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts @@ -41,7 +41,7 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo polarAreaChartConfigTemplate: TemplateRef; predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, protected fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index 5911a60215..9e525cd794 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -142,7 +142,6 @@ export abstract class TbLatestChart { public update(): void { for (const dsData of this.ctx.data) { - console.log("this.ctx.data",this.ctx.data) let value = 0; const tsValue = dsData.data[0]; const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 5585496ede..0ce6a4b290 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,8 +381,8 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: string | ValueSourceConfig; - max?: string | ValueSourceConfig; + min?: number | string | ValueSourceConfig; + max?: number | string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } 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 3f1e96ca84..66d0234914 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 @@ -120,7 +120,7 @@
+ formControlName="animation">
widget-config.card-appearance
@@ -256,8 +256,8 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + [series]="false">
@@ -303,9 +303,9 @@
widgets.bar-chart.bar-appearance
+ formControlName="barSettings" + pieLabelPosition + [series]="false">
@@ -434,7 +434,7 @@
+ formControlName="fillAreaSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts index 355dc55207..07d43f4de2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts @@ -26,7 +26,6 @@ import { import { LatestChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component'; -import { ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-polar-area-chart-widget-settings', @@ -53,32 +52,10 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett protected setupLatestChartControls(latestChartWidgetSettingsForm: UntypedFormGroup, settings: WidgetSettings) { latestChartWidgetSettingsForm.addControl('barSettings', this.fb.control(settings.barSettings, [])); - latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin)); - latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax)); + latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin, [])); + latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, [])); latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [Validators.min(0)])); latestChartWidgetSettingsForm.addControl('angleAxisStartAngle', this.fb.control(settings.angleAxisStartAngle, [])); } - protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { - settings.axisMin = this.normalizeAxisLimit(settings.axisMin); - settings.axisMax = this.normalizeAxisLimit(settings.axisMax); - return super.prepareInputSettings(settings); - } - - private normalizeAxisLimit(limit: any): ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number') { - return { - type: ValueSourceType.constant, - value: limit, - entityAlias: null - }; - } - return limit; - } } From f60a0ab915a0bad34cef4d0999c449cb163916af Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 4 Dec 2025 10:30:42 +0200 Subject: [PATCH 015/147] improved validation --- .../common/axis-scale-row.component.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 165857eac3..9962bdeed9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -114,7 +114,6 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } writeValue(value: ValueSourceConfig) { - console.log("value", value); this.modelValue = value; this.limitForm.patchValue( { @@ -147,14 +146,36 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } validate(): ValidationErrors | null { - return this.limitForm.valid ? null : { - axisLimitForm: { - valid: false, - errors: this.getFormErrors() + const type = this.limitForm.get('type')?.value; + const errors: any = {}; + + if (this.limitForm.invalid) { + errors.form = this.getFormErrors(); + } + + if (type === ValueSourceType.latestKey) { + if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { + errors.latestKey = { + valid: false + }; + } + } else if (type === ValueSourceType.entity) { + if (!this.limitForm.get('entityAlias')?.value) { + errors.entityAlias = { + valid: false + }; } - }; + if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { + errors.entityKey = { + valid: false + }; + } + } + + return Object.keys(errors).length ? { axisLimitForm: errors } : null; } + private getFormErrors(): any { const errors: any = {}; Object.keys(this.limitForm.controls).forEach(key => { @@ -175,6 +196,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.entityKeyFormControl.clearValidators(); } else if (type === ValueSourceType.entity) { this.latestKeyFormControl.clearValidators(); + this.limitForm.get('entityAlias').setValidators([Validators.required]); this.entityKeyFormControl.setValidators([Validators.required]); } else { this.latestKeyFormControl.clearValidators(); @@ -184,6 +206,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); } } + private subscribeToTypeChanges() { this.limitForm.controls.type.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) From 48a64e3b10850eaa7f37d4d559db25d0ec96edaf Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 4 Dec 2025 17:22:16 +0200 Subject: [PATCH 016/147] Added support of relations lifecycle updates for propagation CF --- ...CalculatedFieldEntityMessageProcessor.java | 34 ++++++++++++++----- ...alculatedFieldManagerMessageProcessor.java | 3 +- .../propagation/PropagationArgumentEntry.java | 11 ++++++ .../PropagationCalculatedFieldState.java | 20 ++++++++--- .../cf/CalculatedFieldIntegrationTest.java | 16 +++++++++ .../state/PropagationArgumentEntryTest.java | 28 +++++++++++++++ .../PropagationCalculatedFieldStateTest.java | 28 ++++++++++++++- .../configuration/HasRelationPathLevel.java | 24 +++++++++++++ ...opagationCalculatedFieldConfiguration.java | 2 +- ...onPathQueryDynamicSourceConfiguration.java | 2 -- ...gregationCalculatedFieldConfiguration.java | 3 +- .../common/data/util/CollectionsUtil.java | 8 +++++ 12 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 2685e7f434..d3fcb8e2d8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -56,6 +57,8 @@ import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAg import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.ArrayList; import java.util.Collection; @@ -71,6 +74,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** @@ -225,17 +229,27 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var state = states.get(ctx.getCfId()); try { - Map updatedArgs = new HashMap<>(); if (state == null) { state = createState(ctx); - } else { - if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { - Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); - updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + Map updatedArgs = null; + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); + updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + if (state instanceof PropagationCalculatedFieldState propagationState) { + PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); + boolean added = propagationArgument.addPropagationEntityId(msg.getRelatedEntityId()); + if (added) { + updatedArgs = Map.of(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(List.of(msg.getRelatedEntityId()))); } + } - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + if (CollectionsUtil.isEmpty(updatedArgs)) { + msg.getCallback().onSuccess(); + return; } + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); if (state.isSizeOk()) { processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, callback); } else { @@ -268,9 +282,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } - } else { - msg.getCallback().onSuccess(); + return; } + if (state instanceof PropagationCalculatedFieldState propagationState) { + PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); + propagationArgument.removePropagationEntityId(msg.getRelatedEntityId()); + } + msg.getCallback().onSuccess(); } public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b19ad1a8b4..838b25fca0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -363,7 +364,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware List matchingCfs = cfsByEntityIdAndProfile.stream() .filter(cf -> { - if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + if (cf.getCalculatedField().getConfiguration() instanceof HasRelationPathLevel config) { RelationPathLevel relation = config.getRelation(); return direction.equals(relation.direction()) && relationType.equals(relation.relationType()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index 81009da5e5..ebb23a0794 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -70,4 +70,15 @@ public class PropagationArgumentEntry implements ArgumentEntry { return new TbelCfPropagationArg(propagationEntityIds); } + public boolean addPropagationEntityId(EntityId propagationEntityId) { + if (propagationEntityIds.contains(propagationEntityId)) { + return false; + } + return propagationEntityIds.add(propagationEntityId); + } + + public void removePropagationEntityId(EntityId relatedEntityId) { + propagationEntityIds.remove(relatedEntityId); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 32714b9b65..991d3e66dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; @@ -34,6 +35,7 @@ import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; @@ -63,20 +65,26 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState @Override public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { - ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); - if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry) || propagationArgumentEntry.isEmpty()) { + List propagationEntityIds; + if (CollectionsUtil.isNotEmpty(updatedArgs) && updatedArgs.size() == 1 && updatedArgs.containsKey(PROPAGATION_CONFIG_ARGUMENT)) { + propagationEntityIds = ((PropagationArgumentEntry) updatedArgs.get(PROPAGATION_CONFIG_ARGUMENT)).getPropagationEntityIds(); + } else { + PropagationArgumentEntry propagationArgumentEntry = (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); + propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); + } + if (propagationEntityIds.isEmpty()) { return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); } if (ctx.isApplyExpressionForResolvedArguments()) { return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> PropagationCalculatedFieldResult.builder() - .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .propagationEntityIds(propagationEntityIds) .result((TelemetryCalculatedFieldResult) telemetryCfResult) .build(), MoreExecutors.directExecutor()); } return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() - .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .propagationEntityIds(propagationEntityIds) .result(toTelemetryResult(ctx)) .build()); } @@ -105,4 +113,8 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState return telemetryCfBuilder.build(); } + public PropagationArgumentEntry getPropagationArgument() { + return (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); + } + } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index af0c1b9909..cdb65425dc 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -1146,6 +1146,22 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); }); + + Asset asset3 = createAsset("Propagated Asset 3", null); + EntityRelation rel3 = new EntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + doPost("/api/relation", rel3).andExpect(status().isOk()); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only to new entity after relation creation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed"); + assertThat(telemetry).isNotNull(); + assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + }); + } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java index 14a1b629c1..32e31e7a9e 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -124,4 +124,32 @@ public class PropagationArgumentEntryTest { assertThat((List) tbelCfPropagationArg.getValue()).isEmpty(); } + @Test + void testAddNewPropagationEntityIdToEmptyArgument() { + PropagationArgumentEntry empty = new PropagationArgumentEntry(List.of()); + assertThat(empty.addPropagationEntityId(ENTITY_1_ID)).isTrue(); + assertThat(empty.getPropagationEntityIds()).containsExactly(ENTITY_1_ID); + } + + @Test + void testAddNewPropagationEntityIdThatAlreadyExists() { + PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); + assertThat(hasEntity.addPropagationEntityId(ENTITY_1_ID)).isFalse(); + assertThat(hasEntity.getPropagationEntityIds()).containsExactly(ENTITY_1_ID); + } + + @Test + void testAddNewPropagationEntityId() { + PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); + assertThat(hasEntity.addPropagationEntityId(ENTITY_3_ID)).isTrue(); + assertThat(hasEntity.getPropagationEntityIds()).contains(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + } + + @Test + void testRemovePropagationEntityId() { + PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); + hasEntity.removePropagationEntityId(ENTITY_1_ID); + assertThat(hasEntity.isEmpty()).isTrue(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 202b88b2eb..99c8f5631b 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -221,6 +221,28 @@ public class PropagationCalculatedFieldStateTest { assertThat(result.getResult()).isEqualTo(expectedNode); } + @Test + void testPropagationWithUpdatedPropagationArgument() throws ExecutionException, InterruptedException { + initCtxAndState(false); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + + PropagationArgumentEntry propagationArgument = state.getPropagationArgument(); + assertThat(propagationArgument).isNotNull().isEqualTo(propagationArgEntry); + + AssetId newEntityId = new AssetId(UUID.fromString("83e2c962-eeae-4708-984e-e6a24760f9c3")); + boolean added = propagationArgument.addPropagationEntityId(newEntityId); + assertThat(added).isTrue(); + + ArgumentEntry argumentEntry = state.getArguments().get(PROPAGATION_CONFIG_ARGUMENT); + assertThat(argumentEntry).isNotNull().isInstanceOf(PropagationArgumentEntry.class); + assertThat(((PropagationArgumentEntry) argumentEntry).getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1, newEntityId); + + PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(Map.of(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(List.of(newEntityId)))); + assertThat(propagationCalculatedFieldResult).isNotNull(); + assertThat(propagationCalculatedFieldResult.getPropagationEntityIds()).isNotNull().containsExactly(newEntityId); + } + private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setTenantId(TENANT_ID); @@ -254,6 +276,10 @@ public class PropagationCalculatedFieldStateTest { } private PropagationCalculatedFieldResult performCalculation() throws ExecutionException, InterruptedException { - return (PropagationCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + return performCalculation(Collections.emptyMap()); + } + + private PropagationCalculatedFieldResult performCalculation(Map updatedArgs) throws ExecutionException, InterruptedException { + return (PropagationCalculatedFieldResult) state.performCalculation(updatedArgs, ctx).get(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java new file mode 100644 index 0000000000..40e7441a9b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasRelationPathLevel.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +public interface HasRelationPathLevel { + + RelationPathLevel getRelation(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java index 61d4542eb9..0044822555 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java @@ -27,7 +27,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) -public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration { +public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements HasRelationPathLevel { public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java index dc92ff3685..6595e00f1a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -25,7 +24,6 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.util.CollectionsUtil; import java.util.List; -import java.util.NoSuchElementException; @Data public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java index 0dee6ee4a4..ecbab0ab94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java @@ -22,6 +22,7 @@ import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.relation.RelationPathLevel; @@ -29,7 +30,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import java.util.Map; @Data -public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { +public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasRelationPathLevel { @NotNull private RelationPathLevel relation; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index 0d69db556d..476b63f635 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -137,4 +137,12 @@ public class CollectionsUtil { return (Set) Set.of(newSet.toArray()); } + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + } From 8bbfcca578d9e695d9907d775ec7152204747470 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 4 Dec 2025 18:19:26 +0200 Subject: [PATCH 017/147] Added propagation entities to proto state && added reset of readiness status on relation updates --- .../CalculatedFieldEntityMessageProcessor.java | 6 +++++- .../propagation/PropagationArgumentEntry.java | 4 ++-- .../PropagationCalculatedFieldState.java | 4 ++++ .../server/utils/CalculatedFieldUtils.java | 14 ++++++++++++++ .../server/utils/CalculatedFieldUtilsTest.java | 4 ++-- common/proto/src/main/proto/queue.proto | 1 + 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index d3fcb8e2d8..9d8231956c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -241,6 +241,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); boolean added = propagationArgument.addPropagationEntityId(msg.getRelatedEntityId()); if (added) { + propagationState.resetReadinessStatus(); updatedArgs = Map.of(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(List.of(msg.getRelatedEntityId()))); } } @@ -286,7 +287,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } if (state instanceof PropagationCalculatedFieldState propagationState) { PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); - propagationArgument.removePropagationEntityId(msg.getRelatedEntityId()); + boolean removed = propagationArgument.removePropagationEntityId(msg.getRelatedEntityId()); + if (removed) { + propagationState.resetReadinessStatus(); + } } msg.getCallback().onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index ebb23a0794..964aa5487e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -77,8 +77,8 @@ public class PropagationArgumentEntry implements ArgumentEntry { return propagationEntityIds.add(propagationEntityId); } - public void removePropagationEntityId(EntityId relatedEntityId) { - propagationEntityIds.remove(relatedEntityId); + public boolean removePropagationEntityId(EntityId relatedEntityId) { + return propagationEntityIds.remove(relatedEntityId); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 991d3e66dd..a6abeb1b32 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -117,4 +117,8 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState return (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); } + public void resetReadinessStatus() { + readinessStatus = checkReadiness(requiredArguments, arguments); + } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 09ba7917e9..5fd48398e4 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProt import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.EntityIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; @@ -57,9 +58,11 @@ import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -67,6 +70,8 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + public class CalculatedFieldUtils { public static CalculatedFieldIdProto toProto(CalculatedFieldId cfId) { @@ -105,6 +110,7 @@ public class CalculatedFieldUtils { case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); + case PROPAGATION -> builder.addAllPropagationEntityIds(toPropagationEntityIdsProto((PropagationArgumentEntry) argEntry)); case RELATED_ENTITIES -> { RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; relatedEntitiesArgumentEntry.getEntityInputs() @@ -133,6 +139,10 @@ public class CalculatedFieldUtils { return builder.build(); } + private static List toPropagationEntityIdsProto(PropagationArgumentEntry argEntry) { + return argEntry.getPropagationEntityIds().stream().map(ProtoUtils::toProto).collect(Collectors.toList()); + } + private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { return AlarmRuleStateProto.newBuilder() .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) @@ -269,6 +279,10 @@ public class CalculatedFieldUtils { proto.getGeofencingArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); } + case PROPAGATION -> { + List propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList(); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds)); + } case ALARM -> { AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; AlarmStateProto alarmStateProto = proto.getAlarmState(); diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index db24e51123..9573c9fa35 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -119,7 +119,7 @@ class CalculatedFieldUtilsTest { } @Test - void toProtoAndFromProto_shouldCreatePropagationStateWithoutPropagationArgument() { + void toProtoAndFromProto_shouldCreatePropagationStateWithPropagationArgument() { // given CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); given(stateId.tenantId()).willReturn(TENANT_ID); @@ -158,7 +158,7 @@ class CalculatedFieldUtilsTest { assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); assertThat(propagationState.getArguments()).isNotNull(); - assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNull(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); assertThat(propagationState.getRequiredArguments()).isNull(); assertThat(propagationState.getReadinessStatus()).isNull(); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 9fb8528bce..3cbc84ba1a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -935,6 +935,7 @@ message CalculatedFieldStateProto { int64 lastArgsUpdateTs = 7; int64 lastMetricsEvalTs = 8; repeated ArgumentIntervalProto aggregationArguments = 9; + repeated EntityIdProto propagationEntityIds = 10; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. From 1641b6a4911d8109d3714f268e6522f2cb11df86 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 5 Dec 2025 10:38:08 +0200 Subject: [PATCH 018/147] removed redundant permission checks --- .../controller/AssetProfileController.java | 5 +---- .../server/controller/CustomerController.java | 13 ++--------- .../controller/RuleChainController.java | 12 +--------- .../server/controller/TenantController.java | 13 ++--------- .../server/controller/UserController.java | 22 ++++++++++--------- .../controller/WidgetsBundleController.java | 16 ++------------ .../DefaultAccessControlService.java | 14 ++++-------- 7 files changed, 24 insertions(+), 71 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 8f0b7480ab..9c12880e96 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -237,16 +237,13 @@ public class AssetProfileController extends BaseController { @RequestParam("assetProfileIds") String[] strAssetProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("assetProfileIds", strAssetProfileIds); SecurityUser user = getCurrentUser(); - if (!accessControlService.hasPermission(user, Resource.ASSET_PROFILE, Operation.READ)) { - return Collections.emptyList(); - } TenantId tenantId = user.getTenantId(); List assetProfileIds = new ArrayList<>(); for (String strAssetProfileId : strAssetProfileIds) { assetProfileIds.add(new AssetProfileId(toUUID(strAssetProfileId))); } - return checkNotNull(assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get()); + return assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index e0f8cef1c8..ad547d3cc3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -194,7 +194,7 @@ public class CustomerController extends BaseController { @ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", notes = "Returns a list of Customer objects based on the provided ids." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/customers", params = {"customerIds"}) public List getCustomersByIds( @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @@ -206,16 +206,7 @@ public class CustomerController extends BaseController { for (String strCustomerId : strCustomerIds) { customerIds.add(new CustomerId(toUUID(strCustomerId))); } - return Objects.requireNonNull(checkNotNull(customerService.findCustomersByTenantIdAndIdsAsync(tenantId, customerIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.CUSTOMER, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + return customerService.findCustomersByTenantIdAndIdsAsync(tenantId, customerIds).get(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 4349ea22bf..c1aa3acc89 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -79,7 +79,6 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentMap; @@ -598,16 +597,7 @@ public class RuleChainController extends BaseController { for (String strRuleChainId : strRuleChainIds) { ruleChainIds.add(new RuleChainId(toUUID(strRuleChainId))); } - return Objects.requireNonNull(checkNotNull(ruleChainService.findRuleChainsByIdsAsync(tenantId, ruleChainIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.RULE_CHAIN, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + return ruleChainService.findRuleChainsByIdsAsync(tenantId, ruleChainIds).get(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 22ceb33b8d..8187b3018e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -174,7 +174,7 @@ public class TenantController extends BaseController { return checkNotNull(tenantService.findTenantInfos(pageLink)); } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/tenants", params = {"tenantIds"}) public List getTenantsByIds( @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @@ -186,16 +186,7 @@ public class TenantController extends BaseController { for (String strTenantId : strTenantIds) { tenantIds.add(new TenantId(toUUID(strTenantId))); } - return Objects.requireNonNull(checkNotNull(tenantService.findTenantsByIdsAsync(tenantId, tenantIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.TENANT, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + return tenantService.findTenantsByIdsAsync(tenantId, tenantIds).get(); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e0adf27a97..8da3978aeb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -611,16 +611,18 @@ public class UserController extends BaseController { for (String strUserId : strUserIds) { userIds.add(new UserId(toUUID(strUserId))); } - return Objects.requireNonNull(checkNotNull(userService.findUsersByTenantIdAndIdsAsync(tenantId, userIds).get())) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(user, Resource.USER, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); + List users = checkNotNull(userService.findUsersByTenantIdAndIdsAsync(tenantId, userIds).get()); + return filterUsersByReadPermission(users); + } + + private List filterUsersByReadPermission(List users) { + return users.stream().filter(user -> { + try { + return accessControlService.hasPermission(getCurrentUser(), Resource.USER, Operation.READ, user.getId(), user); + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); } private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 1060c7fe94..021aaf9d13 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -253,23 +253,11 @@ public class WidgetsBundleController extends BaseController { for (String strWidgetsBundleId : strWidgetsBundleIds) { widgetsBundleIds.add(new WidgetsBundleId(toUUID(strWidgetsBundleId))); } - List result; if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - result = checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get()); + return widgetsBundleService.findSystemWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get(); } else { - result = checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get()); + return widgetsBundleService.findAllTenantWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get(); } - - return Objects.requireNonNull(result) - .stream() - .filter(e -> { - try { - return accessControlService.hasPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, Operation.READ, e.getId(), e); - } catch (ThingsboardException ex) { - return false; - } - }) - .toList(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index f8898f9660..055025dd30 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -58,11 +58,8 @@ public class DefaultAccessControlService implements AccessControlService { @Override @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException { - PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); - if (permissionChecker != null) { - return permissionChecker.hasPermission(user, operation); - } - return false; + var permissionChecker = getPermissionChecker(user.getAuthority(), resource); + return permissionChecker.hasPermission(user, operation); } @Override @@ -78,11 +75,8 @@ public class DefaultAccessControlService implements AccessControlService { @Override @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException { - PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); - if (permissionChecker != null) { - return permissionChecker.hasPermission(user, operation, entityId, entity); - } - return false; + var permissionChecker = getPermissionChecker(user.getAuthority(), resource); + return permissionChecker.hasPermission(user, operation, entityId, entity); } private PermissionChecker getPermissionChecker(Authority authority, Resource resource) throws ThingsboardException { From e36f7d3375ef6af7254c96cdb288d35f7347cb54 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 5 Dec 2025 11:51:49 +0200 Subject: [PATCH 019/147] additional trace logs for CFs in msa --- msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml index aa88f0bbb0..dc1c94bcd2 100644 --- a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml +++ b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml @@ -49,6 +49,8 @@ + + From 287e6b950e8b97bf223530dc693beda08ad5344b Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 5 Dec 2025 13:00:01 +0200 Subject: [PATCH 020/147] fixed UserController getUsersByIds method --- .../java/org/thingsboard/server/controller/UserController.java | 2 +- .../org/thingsboard/server/controller/UserControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 8da3978aeb..27a55353ba 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -599,7 +599,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Users By Ids (getUsersByIds)", notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/users", params = {"userIds"}) public List getUsersByIds( @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index 4379b57b45..f5f51a0c6b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -269,7 +269,7 @@ public class UserControllerTest extends AbstractControllerTest { @Test public void testFindUsersByIds() throws Exception { - loginSysAdmin(); + loginTenantAdmin(); List savedUsers = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = createTenantAdminUser(); From 94c2c173de2da4d61ff35f54a808443a6ebd66eb Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 5 Dec 2025 14:08:23 +0200 Subject: [PATCH 021/147] propagation logic for add, remove relations refactoring --- ...CalculatedFieldEntityMessageProcessor.java | 39 ++++---- ...faultCalculatedFieldProcessingService.java | 2 +- .../cf/PropagationCalculatedFieldResult.java | 4 +- .../propagation/PropagationArgumentEntry.java | 51 +++++----- .../PropagationCalculatedFieldState.java | 34 +++---- .../server/utils/CalculatedFieldUtils.java | 24 +---- .../state/PropagationArgumentEntryTest.java | 95 +++++++++++-------- .../PropagationCalculatedFieldStateTest.java | 23 ++--- .../utils/CalculatedFieldUtilsTest.java | 5 +- common/proto/src/main/proto/queue.proto | 1 - 10 files changed, 139 insertions(+), 139 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 9d8231956c..209c2d88a0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -229,26 +229,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var state = states.get(ctx.getCfId()); try { + Map updatedArgs = null; if (state == null) { state = createState(ctx); - } - Map updatedArgs = null; - if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { - Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); - updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); - } - if (state instanceof PropagationCalculatedFieldState propagationState) { - PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); - boolean added = propagationArgument.addPropagationEntityId(msg.getRelatedEntityId()); - if (added) { - propagationState.resetReadinessStatus(); - updatedArgs = Map.of(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(List.of(msg.getRelatedEntityId()))); + } else { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); + updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + if (state instanceof PropagationCalculatedFieldState propagationState) { + PropagationArgumentEntry entry = new PropagationArgumentEntry(); + entry.setAdded(msg.getRelatedEntityId()); + updatedArgs = propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx); + } + if (CollectionsUtil.isEmpty(updatedArgs)) { + msg.getCallback().onSuccess(); + return; } - } - - if (CollectionsUtil.isEmpty(updatedArgs)) { - msg.getCallback().onSuccess(); - return; } state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); if (state.isSizeOk()) { @@ -286,11 +283,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return; } if (state instanceof PropagationCalculatedFieldState propagationState) { - PropagationArgumentEntry propagationArgument = propagationState.getPropagationArgument(); - boolean removed = propagationArgument.removePropagationEntityId(msg.getRelatedEntityId()); - if (removed) { - propagationState.resetReadinessStatus(); - } + PropagationArgumentEntry entry = new PropagationArgumentEntry(); + entry.setRemoved(msg.getRelatedEntityId()); + propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx); } msg.getCallback().onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 818a972251..271fdb828d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -171,7 +171,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, TriConsumer telemetryResultHandler) { - List propagationEntityIds = propagationResult.getPropagationEntityIds(); + List propagationEntityIds = propagationResult.getEntityIds(); if (propagationEntityIds.isEmpty()) { callback.onSuccess(); return; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java index a6d9e203cb..09ec5b6ed7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -28,7 +28,7 @@ import java.util.List; @Builder public final class PropagationCalculatedFieldResult implements CalculatedFieldResult { - private final List propagationEntityIds; + private final List entityIds; private final TelemetryCalculatedFieldResult result; @Override @@ -43,7 +43,7 @@ public final class PropagationCalculatedFieldResult implements CalculatedFieldRe @Override public boolean isEmpty() { - return CollectionsUtil.isEmpty(propagationEntityIds) || result.isEmpty(); + return CollectionsUtil.isEmpty(entityIds) || result.isEmpty(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index 964aa5487e..0450a0599a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -19,22 +19,30 @@ import lombok.Data; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfPropagationArg; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Data public class PropagationArgumentEntry implements ArgumentEntry { - private List propagationEntityIds; + private Set entityIds; + private transient EntityId added; + private transient EntityId removed; private boolean forceResetPrevious; - public PropagationArgumentEntry(List propagationEntityIds) { - this.propagationEntityIds = new ArrayList<>(propagationEntityIds); + public PropagationArgumentEntry() { + this.entityIds = new HashSet<>(); + this.added = null; + this.removed = null; + } + + public PropagationArgumentEntry(List entityIds) { + this.entityIds = new HashSet<>(entityIds); } @Override @@ -44,7 +52,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { @Override public Object getValue() { - return propagationEntityIds; + return entityIds; } @Override @@ -52,33 +60,32 @@ public class PropagationArgumentEntry implements ArgumentEntry { if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) { throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); } + if (propagationArgumentEntry.getAdded() != null) { + boolean updated = entityIds.add(propagationArgumentEntry.getAdded()); + if (updated) { + added = propagationArgumentEntry.getAdded(); + } + return updated; + } + if (propagationArgumentEntry.getRemoved() != null) { + return entityIds.remove(propagationArgumentEntry.getRemoved()); + } if (propagationArgumentEntry.isEmpty()) { - propagationEntityIds.clear(); - } else { - propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); + entityIds.clear(); + return true; } + entityIds = propagationArgumentEntry.getEntityIds(); return true; } @Override public boolean isEmpty() { - return CollectionsUtil.isEmpty(propagationEntityIds); + return entityIds.isEmpty(); } @Override public TbelCfArg toTbelCfArg() { - return new TbelCfPropagationArg(propagationEntityIds); - } - - public boolean addPropagationEntityId(EntityId propagationEntityId) { - if (propagationEntityIds.contains(propagationEntityId)) { - return false; - } - return propagationEntityIds.add(propagationEntityId); - } - - public boolean removePropagationEntityId(EntityId relatedEntityId) { - return propagationEntityIds.remove(relatedEntityId); + return new TbelCfPropagationArg(entityIds); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index a6abeb1b32..7a182d0a84 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; @@ -37,6 +36,7 @@ import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; @@ -65,26 +65,30 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState @Override public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { - List propagationEntityIds; - if (CollectionsUtil.isNotEmpty(updatedArgs) && updatedArgs.size() == 1 && updatedArgs.containsKey(PROPAGATION_CONFIG_ARGUMENT)) { - propagationEntityIds = ((PropagationArgumentEntry) updatedArgs.get(PROPAGATION_CONFIG_ARGUMENT)).getPropagationEntityIds(); - } else { - PropagationArgumentEntry propagationArgumentEntry = (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); - propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); - } - if (propagationEntityIds.isEmpty()) { + ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry)) { return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); } + List entityIds; + if (propagationArgumentEntry.getAdded() != null) { + entityIds = List.of(propagationArgumentEntry.getAdded()); + propagationArgumentEntry.setAdded(null); + } else { + entityIds = List.copyOf(propagationArgumentEntry.getEntityIds()); + if (entityIds.isEmpty()) { + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); + } + } if (ctx.isApplyExpressionForResolvedArguments()) { return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> PropagationCalculatedFieldResult.builder() - .propagationEntityIds(propagationEntityIds) + .entityIds(entityIds) .result((TelemetryCalculatedFieldResult) telemetryCfResult) .build(), MoreExecutors.directExecutor()); } return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() - .propagationEntityIds(propagationEntityIds) + .entityIds(entityIds) .result(toTelemetryResult(ctx)) .build()); } @@ -113,12 +117,4 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState return telemetryCfBuilder.build(); } - public PropagationArgumentEntry getPropagationArgument() { - return (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); - } - - public void resetReadinessStatus() { - readinessStatus = checkReadiness(requiredArguments, arguments); - } - } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 5fd48398e4..7046af3d9c 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -33,7 +33,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProt import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; -import org.thingsboard.server.gen.transport.TransportProtos.EntityIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; @@ -62,7 +61,6 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgume import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -110,7 +108,6 @@ public class CalculatedFieldUtils { case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); - case PROPAGATION -> builder.addAllPropagationEntityIds(toPropagationEntityIdsProto((PropagationArgumentEntry) argEntry)); case RELATED_ENTITIES -> { RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; relatedEntitiesArgumentEntry.getEntityInputs() @@ -139,10 +136,6 @@ public class CalculatedFieldUtils { return builder.build(); } - private static List toPropagationEntityIdsProto(PropagationArgumentEntry argEntry) { - return argEntry.getPropagationEntityIds().stream().map(ProtoUtils::toProto).collect(Collectors.toList()); - } - private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { return AlarmRuleStateProto.newBuilder() .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) @@ -271,18 +264,11 @@ public class CalculatedFieldUtils { state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); switch (type) { - case SCRIPT -> { - proto.getRollingValueArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); - } - case GEOFENCING -> { - proto.getGeofencingArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); - } - case PROPAGATION -> { - List propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList(); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds)); - } + case SCRIPT -> proto.getRollingValueArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + case GEOFENCING -> proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + case PROPAGATION -> state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry()); case ALARM -> { AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; AlarmStateProto alarmStateProto = proto.getAlarmState(); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java index 32e31e7a9e..bf6a112e72 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -25,6 +25,7 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgume import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -59,10 +60,10 @@ public class PropagationArgumentEntryTest { @Test void testGetValueReturnsPropagationIds() { - assertThat(entry.getValue()).isInstanceOf(List.class); + assertThat(entry.getValue()).isInstanceOf(Set.class); @SuppressWarnings("unchecked") - List value = (List) entry.getValue(); - assertThat(value).containsExactly(ENTITY_1_ID, ENTITY_2_ID); + Set value = (Set) entry.getValue(); + assertThat(value).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); } @Test @@ -87,7 +88,7 @@ public class PropagationArgumentEntryTest { boolean changed = entry.updateEntry(updated); assertThat(changed).isTrue(); - assertThat(entry.getPropagationEntityIds()).containsExactlyElementsOf(newIds); + assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds); } @Test @@ -97,59 +98,79 @@ public class PropagationArgumentEntryTest { boolean changed = entry.updateEntry(updatedEmpty); assertThat(changed).isTrue(); - assertThat(entry.getPropagationEntityIds()).isEmpty(); + assertThat(entry.getEntityIds()).isEmpty(); } @Test - @SuppressWarnings("unchecked") - void testToTbelCfArgWithValues() { - TbelCfArg arg = entry.toTbelCfArg(); - assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); + void testUpdateEntryWhenAdded() { + var added = new PropagationArgumentEntry(); + added.setAdded(ENTITY_3_ID); - TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; - assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); - assertThat((List) tbelCfPropagationArg.getValue()).containsExactly(ENTITY_1_ID, ENTITY_2_ID); - } + boolean changed = entry.updateEntry(added); + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).isEqualTo(ENTITY_3_ID); + } @Test - @SuppressWarnings("unchecked") - void testToTbelCfArgWithEmptyValues() { - var empty = new PropagationArgumentEntry(List.of()); - TbelCfArg emptyArg = empty.toTbelCfArg(); - assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); + void testUpdateEntryWhenAddedExistingEntity() { + var added = new PropagationArgumentEntry(); + added.setAdded(ENTITY_2_ID); - TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; - assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); - assertThat((List) tbelCfPropagationArg.getValue()).isEmpty(); + boolean changed = entry.updateEntry(added); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getAdded()).isNull(); } @Test - void testAddNewPropagationEntityIdToEmptyArgument() { - PropagationArgumentEntry empty = new PropagationArgumentEntry(List.of()); - assertThat(empty.addPropagationEntityId(ENTITY_1_ID)).isTrue(); - assertThat(empty.getPropagationEntityIds()).containsExactly(ENTITY_1_ID); + void testUpdateEntryWhenRemoved() { + var removed = new PropagationArgumentEntry(); + removed.setRemoved(ENTITY_2_ID); + + boolean changed = entry.updateEntry(removed); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); + assertThat(entry.getRemoved()).isNull(); } @Test - void testAddNewPropagationEntityIdThatAlreadyExists() { - PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); - assertThat(hasEntity.addPropagationEntityId(ENTITY_1_ID)).isFalse(); - assertThat(hasEntity.getPropagationEntityIds()).containsExactly(ENTITY_1_ID); + void testUpdateEntryWhenRemovedNonExistingEntity() { + var removed = new PropagationArgumentEntry(); + removed.setRemoved(ENTITY_3_ID); + + boolean changed = entry.updateEntry(removed); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getRemoved()).isNull(); } @Test - void testAddNewPropagationEntityId() { - PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); - assertThat(hasEntity.addPropagationEntityId(ENTITY_3_ID)).isTrue(); - assertThat(hasEntity.getPropagationEntityIds()).contains(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + @SuppressWarnings("unchecked") + void testToTbelCfArgWithValues() { + TbelCfArg arg = entry.toTbelCfArg(); + assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(Set.class); + assertThat((Set) tbelCfPropagationArg.getValue()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); } + @Test - void testRemovePropagationEntityId() { - PropagationArgumentEntry hasEntity = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); - hasEntity.removePropagationEntityId(ENTITY_1_ID); - assertThat(hasEntity.isEmpty()).isTrue(); + @SuppressWarnings("unchecked") + void testToTbelCfArgWithEmptyValues() { + var empty = new PropagationArgumentEntry(List.of()); + TbelCfArg emptyArg = empty.toTbelCfArg(); + assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(Set.class); + assertThat((Set) tbelCfPropagationArg.getValue()).isEmpty(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 99c8f5631b..add6c1ee39 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -172,7 +172,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.isEmpty()).isTrue(); - assertThat(result.getPropagationEntityIds()).isNullOrEmpty(); + assertThat(result.getEntityIds()).isNullOrEmpty(); } @Test @@ -185,7 +185,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(propagationResult).isNotNull(); assertThat(propagationResult.isEmpty()).isFalse(); - assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + assertThat(propagationResult.getEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); TelemetryCalculatedFieldResult result = propagationResult.getResult(); assertThat(result).isNotNull(); @@ -208,7 +208,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(propagationResult).isNotNull(); assertThat(propagationResult.isEmpty()).isFalse(); - assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + assertThat(propagationResult.getEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); TelemetryCalculatedFieldResult result = propagationResult.getResult(); assertThat(result).isNotNull(); @@ -227,20 +227,15 @@ public class PropagationCalculatedFieldStateTest { state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - PropagationArgumentEntry propagationArgument = state.getPropagationArgument(); - assertThat(propagationArgument).isNotNull().isEqualTo(propagationArgEntry); - AssetId newEntityId = new AssetId(UUID.fromString("83e2c962-eeae-4708-984e-e6a24760f9c3")); - boolean added = propagationArgument.addPropagationEntityId(newEntityId); - assertThat(added).isTrue(); - - ArgumentEntry argumentEntry = state.getArguments().get(PROPAGATION_CONFIG_ARGUMENT); - assertThat(argumentEntry).isNotNull().isInstanceOf(PropagationArgumentEntry.class); - assertThat(((PropagationArgumentEntry) argumentEntry).getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1, newEntityId); + PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(); + propagationArgumentEntry.setAdded(newEntityId); + Map updated = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry), ctx); + assertThat(updated).isNotNull().containsEntry(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry); - PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(Map.of(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(List.of(newEntityId)))); + PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(updated); assertThat(propagationCalculatedFieldResult).isNotNull(); - assertThat(propagationCalculatedFieldResult.getPropagationEntityIds()).isNotNull().containsExactly(newEntityId); + assertThat(propagationCalculatedFieldResult.getEntityIds()).isNotNull().containsExactly(newEntityId); } private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 9573c9fa35..83538fe07c 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -119,7 +119,7 @@ class CalculatedFieldUtilsTest { } @Test - void toProtoAndFromProto_shouldCreatePropagationStateWithPropagationArgument() { + void toProtoAndFromProto_shouldCreatePropagationStateWithEmptyPropagationArgument() { // given CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); given(stateId.tenantId()).willReturn(TENANT_ID); @@ -158,7 +158,8 @@ class CalculatedFieldUtilsTest { assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); assertThat(propagationState.getArguments()).isNotNull(); - assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNotNull(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT).isEmpty()).isTrue(); assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); assertThat(propagationState.getRequiredArguments()).isNull(); assertThat(propagationState.getReadinessStatus()).isNull(); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 3cbc84ba1a..9fb8528bce 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -935,7 +935,6 @@ message CalculatedFieldStateProto { int64 lastArgsUpdateTs = 7; int64 lastMetricsEvalTs = 8; repeated ArgumentIntervalProto aggregationArguments = 9; - repeated EntityIdProto propagationEntityIds = 10; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. From a470cf6523a812ce4392ae8fc67b037c35359b9d Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 5 Dec 2025 15:54:06 +0200 Subject: [PATCH 022/147] Fixed incorrect number of callbacks --- .../CalculatedFieldEntityMessageProcessor.java | 5 ++--- .../state/propagation/PropagationCalculatedFieldState.java | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 209c2d88a0..55635e9f9d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -226,7 +226,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM private void handleRelationUpdate(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { CalculatedFieldCtx ctx = msg.getCalculatedField(); - var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var state = states.get(ctx.getCfId()); try { Map updatedArgs = null; @@ -246,10 +245,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM msg.getCallback().onSuccess(); return; } + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); } - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); if (state.isSizeOk()) { - processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, callback); + processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); } else { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 7a182d0a84..5a7753c86a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -36,7 +36,6 @@ import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; @@ -74,10 +73,10 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState entityIds = List.of(propagationArgumentEntry.getAdded()); propagationArgumentEntry.setAdded(null); } else { - entityIds = List.copyOf(propagationArgumentEntry.getEntityIds()); - if (entityIds.isEmpty()) { + if (propagationArgumentEntry.getEntityIds().isEmpty()) { return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); } + entityIds = List.copyOf(propagationArgumentEntry.getEntityIds()); } if (ctx.isApplyExpressionForResolvedArguments()) { return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> From e37332a288b532c8f2cdfaae16d578122e9658a2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 5 Dec 2025 17:00:02 +0200 Subject: [PATCH 023/147] handle output update to avoid processing when minor strategy properties updated --- ...CalculatedFieldEntityMessageProcessor.java | 10 +- ...alculatedFieldManagerMessageProcessor.java | 2 + .../EntityInitCalculatedFieldMsg.java | 3 +- .../cf/ctx/state/CalculatedFieldCtx.java | 94 ++++++++++++++++--- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 2685e7f434..93fd7498e5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -159,10 +159,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else { state.setCtx(ctx, actorCtx); } - if (state.isSizeOk()) { - processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + if (msg.getStateAction() != StateAction.REFRESH_CTX) { + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + } else { + throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + } } else { - throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + msg.getCallback().onSuccess(); } } catch (Exception e) { log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b19ad1a8b4..97d5d0dcea 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -455,6 +455,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate } else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) { stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate + } else if (newCfCtx.hasRefreshContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REFRESH_CTX; } else { callback.onSuccess(); return; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java index 1e0025988d..49f2c691d3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java @@ -39,6 +39,7 @@ public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg INIT, REINIT, RECREATE, - REPROCESS + REPROCESS, + REFRESH_CTX } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index c2c65d3327..489632b9b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -37,12 +37,14 @@ import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldC import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; @@ -623,16 +625,42 @@ public class CalculatedFieldCtx implements Closeable { return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); } + public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation + var thisConfig = calculatedField.getConfiguration(); + var otherConfig = other.getCalculatedField().getConfiguration(); + + var thisOutputStrategy = thisConfig.getOutput().getStrategy(); + var otherOutputStrategy = otherConfig.getOutput().getStrategy(); + + if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { + return true; + } + + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { + return thisTimeSeriesImmediateOutputStrategy.getTtl() != otherTimeSeriesImmediateOutputStrategy.getTtl(); + } + + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { + boolean updateAttributesOnlyOnValueChangeChanged = thisAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttributesUpdatedNotificationUpdated = thisAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification() != otherAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification(); + return updateAttributesOnlyOnValueChangeChanged || sendAttributesUpdatedNotificationUpdated; + } + + return false; + } + public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { return true; } - if (!Objects.equals(output, other.output)) { + if (hasOutputChanges(other.output)) { return true; } if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig - && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { + && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig + && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { return true; } if (cfType == CalculatedFieldType.ALARM) { @@ -654,14 +682,14 @@ public class CalculatedFieldCtx implements Closeable { return true; } if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig - && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig - && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() + && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig + && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()) || thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) { return true; } if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig - && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); return metricsChanged || watermarkChanged; @@ -695,9 +723,47 @@ public class CalculatedFieldCtx implements Closeable { return false; } + private boolean hasOutputChanges(Output otherOutput) { + if (!output.getType().equals(otherOutput.getType())) { + return true; + } + if (!output.getName().equals(otherOutput.getName())) { + return true; + } + if (output.getScope() != (otherOutput.getScope())) { + return true; + } + if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { + return true; + } + + var thisOutputStrategy = output.getStrategy(); + var otherOutputStrategy = otherOutput.getStrategy(); + + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { + boolean saveTimeSeriesUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveTimeSeries() != otherTimeSeriesImmediateOutputStrategy.isSaveTimeSeries(); + boolean saveLatestUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveLatest() != otherTimeSeriesImmediateOutputStrategy.isSaveLatest(); + boolean sendWsUpdateUpdated = thisTimeSeriesImmediateOutputStrategy.isSendWsUpdate() != otherTimeSeriesImmediateOutputStrategy.isSendWsUpdate(); + boolean processCfsUpdated = thisTimeSeriesImmediateOutputStrategy.isProcessCfs() != otherTimeSeriesImmediateOutputStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { + + boolean saveTimeSeriesUpdated = thisAttributesImmediateOutputStrategy.isSaveAttribute() != otherAttributesImmediateOutputStrategy.isSaveAttribute(); + boolean sendWsUpdateUpdated = thisAttributesImmediateOutputStrategy.isSendWsUpdate() != otherAttributesImmediateOutputStrategy.isSendWsUpdate(); + boolean processCfsUpdated = thisAttributesImmediateOutputStrategy.isProcessCfs() != otherAttributesImmediateOutputStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + return false; + } + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); } return false; @@ -705,7 +771,7 @@ public class CalculatedFieldCtx implements Closeable { private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { return !thisConfig.getRelation().equals(otherConfig.getRelation()); } return false; @@ -713,7 +779,7 @@ public class CalculatedFieldCtx implements Closeable { private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { return !thisConfig.getInterval().equals(otherConfig.getInterval()); } return false; @@ -738,7 +804,7 @@ public class CalculatedFieldCtx implements Closeable { yield true; } yield geofencingState.getLastDynamicArgumentsRefreshTs() < - System.currentTimeMillis() - scheduledUpdateIntervalMillis; + System.currentTimeMillis() - scheduledUpdateIntervalMillis; } default -> false; }; @@ -782,10 +848,10 @@ public class CalculatedFieldCtx implements Closeable { @Override public String toString() { return "CalculatedFieldCtx{" + - "cfId=" + cfId + - ", cfType=" + cfType + - ", entityId=" + entityId + - '}'; + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; } } From fab611bb211303ef8c2f96b3374a084523208c99 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 5 Dec 2025 19:41:43 +0200 Subject: [PATCH 024/147] Added trace level for cluster service & rule engine consumer manager --- msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml index dc1c94bcd2..9c27ebb2a3 100644 --- a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml +++ b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml @@ -48,6 +48,7 @@ + From c2ce03c8c6a10369cd9cb4b33fc59843b136d130 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 5 Dec 2025 19:42:01 +0200 Subject: [PATCH 025/147] Added trace level for cluster service & rule engine consumer manager --- msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml index 9c27ebb2a3..f9fd25d44a 100644 --- a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml +++ b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml @@ -49,6 +49,7 @@ + From 5e7335dd15165919386c8b95208099df251b9009 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 8 Dec 2025 12:54:18 +0200 Subject: [PATCH 026/147] Add API keys to rest client --- .../thingsboard/rest/client/RestClient.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 6eb61b3e13..f917a92e47 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -105,6 +105,7 @@ import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -154,6 +155,8 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -288,6 +291,10 @@ public class RestClient implements Closeable { }); } + public static RestClient withApiKey(String baseURL, String token) { + return withApiKey(new RestTemplate(), baseURL, token); + } + public static RestClient withApiKey(RestTemplate rt, String baseURL, String token) { return new RestClient(rt, baseURL, AuthType.API_KEY, token); } @@ -3038,6 +3045,44 @@ public class RestClient implements Closeable { userCredentialsEnabled); } + public ApiKey saveApiKey(ApiKeyInfo apiKeyInfo) { + return restTemplate.postForEntity(baseURL + "/api/apiKey", apiKeyInfo, ApiKey.class).getBody(); + } + + public PageData getUserApiKeys(UserId userId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("userId", userId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/apiKeys/{userId}?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() {}, params).getBody(); + } + + public ApiKeyInfo updateApiKeyDescription(ApiKeyId apiKeyId, Optional description) { + return restTemplate.exchange( + baseURL + "/api/apiKey/{id}/description", + HttpMethod.PUT, + new HttpEntity<>(description), + ApiKeyInfo.class, + apiKeyId.getId()).getBody(); + } + + public ApiKeyInfo enableApiKey(ApiKeyId apiKeyId, boolean enabled) { + return restTemplate.exchange( + baseURL + "/api/apiKey/{id}/enabled/{enabledValue}", + HttpMethod.PUT, + HttpEntity.EMPTY, + ApiKeyInfo.class, + apiKeyId.getId(), + enabled).getBody(); + } + + public void deleteApiKey(ApiKeyId apiKeyId) { + restTemplate.delete(baseURL + "/api/apiKey/{id}", apiKeyId.getId()); + } + public Optional getWidgetsBundleById(WidgetsBundleId widgetsBundleId) { try { ResponseEntity widgetsBundle = From d62355e4bda4ceeb0061115d4a5f4fe67d450853 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 8 Dec 2025 14:17:09 +0200 Subject: [PATCH 027/147] refactoring --- .../controller/AssetProfileController.java | 17 ++++++-------- .../server/controller/CustomerController.java | 17 ++++++-------- .../controller/DashboardController.java | 13 ++++------- .../controller/DeviceProfileController.java | 23 +++++++------------ .../controller/EntityViewController.java | 14 +++++------ .../controller/RuleChainController.java | 14 +++++------ .../server/controller/TenantController.java | 17 ++++++-------- .../server/controller/UserController.java | 16 ++++++------- .../controller/WidgetsBundleController.java | 17 ++++---------- .../server/dao/asset/AssetProfileService.java | 3 +-- .../server/dao/customer/CustomerService.java | 2 +- .../dao/dashboard/DashboardService.java | 2 +- .../dao/device/DeviceProfileService.java | 3 +-- .../dao/entityview/EntityViewService.java | 2 +- .../server/dao/rule/RuleChainService.java | 2 +- .../server/dao/tenant/TenantService.java | 2 +- .../server/dao/user/UserService.java | 2 +- .../dao/widget/WidgetsBundleService.java | 5 +--- .../server/dao/asset/AssetProfileDao.java | 3 +-- .../dao/asset/AssetProfileServiceImpl.java | 10 +++----- .../server/dao/customer/CustomerDao.java | 3 +-- .../dao/customer/CustomerServiceImpl.java | 9 +++----- .../dao/dashboard/DashboardInfoDao.java | 3 +-- .../dao/dashboard/DashboardServiceImpl.java | 8 +++---- .../server/dao/device/DeviceProfileDao.java | 3 +-- .../dao/device/DeviceProfileServiceImpl.java | 10 +++----- .../server/dao/entityview/EntityViewDao.java | 2 +- .../dao/entityview/EntityViewServiceImpl.java | 9 +++----- .../server/dao/rule/BaseRuleChainService.java | 8 +++---- .../server/dao/rule/RuleChainDao.java | 4 +--- .../dao/sql/asset/JpaAssetProfileDao.java | 5 ++-- .../dao/sql/customer/JpaCustomerDao.java | 5 ++-- .../sql/dashboard/JpaDashboardInfoDao.java | 4 ++-- .../dao/sql/device/JpaDeviceProfileDao.java | 5 ++-- .../dao/sql/entityview/JpaEntityViewDao.java | 4 ++-- .../server/dao/sql/rule/JpaRuleChainDao.java | 5 ++-- .../server/dao/sql/tenant/JpaTenantDao.java | 5 ++-- .../server/dao/sql/user/JpaUserDao.java | 5 ++-- .../dao/sql/widget/JpaWidgetsBundleDao.java | 11 ++------- .../sql/widget/WidgetsBundleRepository.java | 16 ++++--------- .../server/dao/tenant/TenantDao.java | 3 +-- .../server/dao/tenant/TenantServiceImpl.java | 7 +++--- .../thingsboard/server/dao/user/UserDao.java | 3 +-- .../server/dao/user/UserServiceImpl.java | 9 +++----- .../server/dao/widget/WidgetsBundleDao.java | 5 +--- .../dao/widget/WidgetsBundleServiceImpl.java | 19 +++------------ 46 files changed, 127 insertions(+), 227 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 9c12880e96..59cec67be6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -48,9 +48,9 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutionException; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; @@ -234,16 +234,13 @@ public class AssetProfileController extends BaseController { @GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"}) public List getAssetProfilesByIds( @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("assetProfileIds") String[] strAssetProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("assetProfileIds", strAssetProfileIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("assetProfileIds") Set assetProfileUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List assetProfileIds = new ArrayList<>(); - for (String strAssetProfileId : strAssetProfileIds) { - assetProfileIds.add(new AssetProfileId(toUUID(strAssetProfileId))); + for (UUID assetProfileUUID : assetProfileUUIDs) { + assetProfileIds.add(new AssetProfileId(assetProfileUUID)); } - - return assetProfileService.findAssetProfilesByIdsAsync(tenantId, assetProfileIds).get(); + return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index ad547d3cc3..67ce9cc7c4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -42,14 +42,13 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.customer.TbCustomerService; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; @@ -198,15 +197,13 @@ public class CustomerController extends BaseController { @GetMapping(value = "/customers", params = {"customerIds"}) public List getCustomersByIds( @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("customerIds") String[] strCustomerIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("customerIds", strCustomerIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("customerIds") Set customerUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List customerIds = new ArrayList<>(); - for (String strCustomerId : strCustomerIds) { - customerIds.add(new CustomerId(toUUID(strCustomerId))); + for (UUID customerUUID : customerUUIDs) { + customerIds.add(new CustomerId(customerUUID)); } - return customerService.findCustomersByTenantIdAndIdsAsync(tenantId, customerIds).get(); + return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index f23c3d3e19..fe5ba633db 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -70,7 +70,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -621,15 +620,13 @@ public class DashboardController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboards", params = {"dashboardIds"}) public List getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("dashboardIds") String[] strDashboardIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("dashboardIds", strDashboardIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List dashboardIds = new ArrayList<>(); - for (String strDashboardId : strDashboardIds) { - dashboardIds.add(new DashboardId(toUUID(strDashboardId))); + for (UUID dashboardUUID : dashboardUUIDs) { + dashboardIds.add(new DashboardId(dashboardUUID)); } - List dashboards = checkNotNull(dashboardService.findDashboardInfoByIdsAsync(tenantId, dashboardIds).get()); + List dashboards = dashboardService.findDashboardInfoByIds(tenantId, dashboardIds); return filterDashboardsByReadPermission(dashboards); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 7092a48c59..9ead0c099d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -51,10 +51,9 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID; @@ -283,26 +282,20 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); } - @ApiOperation(value = "Get Device Profiles By Ids (getDeviceProfilesByIds)", + @ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfilesByIds)", notes = "Requested device profiles must be owned by tenant which is performing the request. " + NEW_LINE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"}) - public List getDeviceProfilesByIds( + public List getDeviceProfileInfosByIds( @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("deviceProfileIds") String[] strDeviceProfileIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("deviceProfileIds", strDeviceProfileIds); - SecurityUser user = getCurrentUser(); - if (!accessControlService.hasPermission(user, Resource.DEVICE_PROFILE, Operation.READ)) { - return Collections.emptyList(); - } - TenantId tenantId = user.getTenantId(); + @RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List deviceProfileIds = new ArrayList<>(); - for (String strDeviceProfileId : strDeviceProfileIds) { - deviceProfileIds.add(new DeviceProfileId(toUUID(strDeviceProfileId))); + for (UUID deviceProfileUUID : deviceProfileUUIDs) { + deviceProfileIds.add(new DeviceProfileId(deviceProfileUUID)); } - - return checkNotNull(deviceProfileService.findDeviceProfilesByIdsAsync(tenantId, deviceProfileIds).get()); + return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index c67f260422..4455843b99 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -57,6 +57,8 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -468,15 +470,13 @@ public class EntityViewController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/entityViews", params = {"entityViewIds"}) public List getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("entityViewIds") String[] strEntityViewIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("entityViewIds", strEntityViewIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("entityViewIds") Set entityViewUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List entityViewIds = new ArrayList<>(); - for (String strEntityViewId : strEntityViewIds) { - entityViewIds.add(new EntityViewId(toUUID(strEntityViewId))); + for (UUID entityViewUUID : entityViewUUIDs) { + entityViewIds.add(new EntityViewId(entityViewUUID)); } - List entityViews = checkNotNull(entityViewService.findEntityViewsByTenantIdAndIdsAsync(tenantId, entityViewIds).get()); + List entityViews = entityViewService.findEntityViewsByTenantIdAndIds(tenantId, entityViewIds); return filterEntityViewsByReadPermission(entityViews); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index c1aa3acc89..bbc5ef2195 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -72,7 +72,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -81,6 +80,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; @@ -589,15 +589,13 @@ public class RuleChainController extends BaseController { @GetMapping(value = "/ruleChains", params = {"ruleChainIds"}) public List getRuleChainsByIds( @Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("ruleChainIds") String[] strRuleChainIds) throws Exception { - checkArrayParameter("ruleChainIds", strRuleChainIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { + TenantId tenantId = getCurrentUser().getTenantId(); List ruleChainIds = new ArrayList<>(); - for (String strRuleChainId : strRuleChainIds) { - ruleChainIds.add(new RuleChainId(toUUID(strRuleChainId))); + for (UUID ruleChainUUID : ruleChainUUIDs) { + ruleChainIds.add(new RuleChainId(ruleChainUUID)); } - return ruleChainService.findRuleChainsByIdsAsync(tenantId, ruleChainIds).get(); + return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 8187b3018e..7eeb8c7416 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -41,14 +41,13 @@ import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.tenant.TbTenantService; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -178,15 +177,13 @@ public class TenantController extends BaseController { @GetMapping(value = "/tenants", params = {"tenantIds"}) public List getTenantsByIds( @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) - @RequestParam("tenantIds") String[] strTenantIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("tenantIds", strTenantIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("tenantIds") Set tenantUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List tenantIds = new ArrayList<>(); - for (String strTenantId : strTenantIds) { - tenantIds.add(new TenantId(toUUID(strTenantId))); + for (UUID tenantIdUUID : tenantUUIDs) { + tenantIds.add(TenantId.fromUUID(tenantIdUUID)); } - return tenantService.findTenantsByIdsAsync(tenantId, tenantIds).get(); + return tenantService.findTenantsByIds(tenantId, tenantIds); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 27a55353ba..6ec1717745 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -85,8 +85,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ExecutionException; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; @@ -603,15 +603,13 @@ public class UserController extends BaseController { @GetMapping(value = "/users", params = {"userIds"}) public List getUsersByIds( @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("userIds") String[] strUserIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("userIds", strUserIds); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); + @RequestParam("userIds") Set userUUIDs) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); List userIds = new ArrayList<>(); - for (String strUserId : strUserIds) { - userIds.add(new UserId(toUUID(strUserId))); + for (UUID userUUID : userUUIDs) { + userIds.add(new UserId(userUUID)); } - List users = checkNotNull(userService.findUsersByTenantIdAndIdsAsync(tenantId, userIds).get()); + List users = userService.findUsersByTenantIdAndIds(tenantId, userIds); return filterUsersByReadPermission(users); } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 021aaf9d13..cc39d47a9b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -47,12 +47,10 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; -import java.util.Objects; import java.util.Set; -import java.util.concurrent.ExecutionException; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; @@ -247,17 +245,12 @@ public class WidgetsBundleController extends BaseController { @GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"}) public List getWidgetsBundlesByIds( @Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("widgetsBundleIds") String[] strWidgetsBundleIds) throws ThingsboardException, ExecutionException, InterruptedException { - checkArrayParameter("widgetsBundleIds", strWidgetsBundleIds); + @RequestParam("widgetsBundleIds") Set widgetsBundleUUIDs) throws ThingsboardException { List widgetsBundleIds = new ArrayList<>(); - for (String strWidgetsBundleId : strWidgetsBundleIds) { - widgetsBundleIds.add(new WidgetsBundleId(toUUID(strWidgetsBundleId))); - } - if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { - return widgetsBundleService.findSystemWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get(); - } else { - return widgetsBundleService.findAllTenantWidgetsBundlesByIdsAsync(getTenantId(), widgetsBundleIds).get(); + for (UUID widgetsBundleUUID : widgetsBundleUUIDs) { + widgetsBundleIds.add(new WidgetsBundleId(widgetsBundleUUID)); } + return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index e867656d4c..bc7af698f9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.asset; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -63,6 +62,6 @@ public interface AssetProfileService extends EntityDaoService { List findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); - ListenableFuture> findAssetProfilesByIdsAsync(TenantId tenantId, List assetProfileIds); + List findAssetProfilesByIds(TenantId tenantId, List assetProfileIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 82a5605409..a494313291 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -48,6 +48,6 @@ public interface CustomerService extends EntityDaoService { void deleteCustomersByTenantId(TenantId tenantId); - ListenableFuture> findCustomersByTenantIdAndIdsAsync(TenantId tenantId, List customerIds); + List findCustomersByTenantIdAndIds(TenantId tenantId, List customerIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 18efebc92d..1d850ac99b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -80,6 +80,6 @@ public interface DashboardService extends EntityDaoService { PageData findAllDashboardsIds(PageLink pageLink); - ListenableFuture> findDashboardInfoByIdsAsync(TenantId tenantId, List dashboardIds); + List findDashboardInfoByIds(TenantId tenantId, List dashboardIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index 3bbd760eec..81fdcffb3f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.device; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityInfo; @@ -65,6 +64,6 @@ public interface DeviceProfileService extends EntityDaoService { List findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); - ListenableFuture> findDeviceProfilesByIdsAsync(TenantId tenantId, List deviceProfileIds); + List findDeviceProfilesByIds(TenantId tenantId, List deviceProfileIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index e64cd0dd6d..ee2ef71557 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -65,7 +65,7 @@ public interface EntityViewService extends EntityDaoService { PageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); - ListenableFuture> findEntityViewsByTenantIdAndIdsAsync(TenantId tenantId, List entityViewIds); + List findEntityViewsByTenantIdAndIds(TenantId tenantId, List entityViewIds); PageData findEntityViewInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index fd84cc4c82..faeeae0244 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -117,6 +117,6 @@ public interface RuleChainService extends EntityDaoService { void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId); - ListenableFuture> findRuleChainsByIdsAsync(TenantId tenantId, List ruleChainIds); + List findRuleChainsByIds(TenantId tenantId, List ruleChainIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index db9b77e161..d54f46de59 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -55,6 +55,6 @@ public interface TenantService extends EntityDaoService { PageData findTenantsIds(PageLink pageLink); - ListenableFuture> findTenantsByIdsAsync(TenantId callerId, List tenantIds); + List findTenantsByIds(TenantId callerId, List tenantIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 629f1c1db5..d911f69bc7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -111,6 +111,6 @@ public interface UserService extends EntityDaoService { int countTenantAdmins(TenantId tenantId); - ListenableFuture> findUsersByTenantIdAndIdsAsync(TenantId tenantId, List userIds); + List findUsersByTenantIdAndIds(TenantId tenantId, List userIds); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java index 5341b8862b..4c5d7cb086 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.widget; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -53,8 +52,6 @@ public interface WidgetsBundleService extends EntityDaoService { void updateSystemWidgets(Stream bundles, Stream widgets); - ListenableFuture> findSystemWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds); - - ListenableFuture> findAllTenantWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds); + List findSystemOrTenantWidgetsBundlesByIds(TenantId tenantId, List widgetsBundleIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java index d56e794b93..e12ca07999 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.asset; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -52,6 +51,6 @@ public interface AssetProfileDao extends Dao, ExportableEntityDao< List findTenantAssetProfileNames(UUID tenantId, boolean activeOnly); - ListenableFuture> findAssetProfilesByTenantIdAndIdsAsync(UUID tenantId, List assetProfileIds); + List findAssetProfilesByTenantIdAndIds(UUID tenantId, List assetProfileIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 63a5e1b241..86f3cfe130 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.asset; import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -54,7 +53,6 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("AssetProfileDaoService") @Slf4j @@ -349,11 +347,9 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService> findAssetProfilesByIdsAsync(TenantId tenantId, List assetProfileIds) { - log.trace("Executing findAssetProfilesByIdsAsync, tenantId [{}], assetProfileIds [{}]", tenantId, assetProfileIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(assetProfileIds, ids -> "Incorrect assetProfileIds " + ids); - return assetProfileDao.findAssetProfilesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetProfileIds)); + public List findAssetProfilesByIds(TenantId tenantId, List assetProfileIds) { + log.trace("Executing findAssetProfilesByIds, tenantId [{}], assetProfileIds [{}]", tenantId, assetProfileIds); + return assetProfileDao.findAssetProfilesByTenantIdAndIds(tenantId.getId(), toUUIDs(assetProfileIds)); } private final PaginatedRemover tenantAssetProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index 9b3a22eaaa..7b039a6dfd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.customer; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -80,6 +79,6 @@ public interface CustomerDao extends Dao, TenantEntityDao, E */ PageData findCustomersWithTheSameTitle(PageLink pageLink); - ListenableFuture> findCustomersByTenantIdAndIdsAsync(UUID tenantId, List customerIds); + List findCustomersByTenantIdAndIds(UUID tenantId, List customerIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 10299e3223..196cd468ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -59,7 +59,6 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("CustomerDaoService") @Slf4j @@ -266,11 +265,9 @@ public class CustomerServiceImpl extends AbstractCachedEntityService> findCustomersByTenantIdAndIdsAsync(TenantId tenantId, List customerIds) { - log.trace("Executing findCustomersByTenantIdAndIdsAsync, tenantId [{}], customerIds [{}]", tenantId, customerIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(customerIds, ids -> "Incorrect customerIds " + ids); - return customerDao.findCustomersByTenantIdAndIdsAsync(tenantId.getId(), customerIds.stream().map(CustomerId::getId).collect(Collectors.toList())); + public List findCustomersByTenantIdAndIds(TenantId tenantId, List customerIds) { + log.trace("Executing findCustomersByTenantIdAndIds, tenantId [{}], customerIds [{}]", tenantId, customerIds); + return customerDao.findCustomersByTenantIdAndIds(tenantId.getId(), customerIds.stream().map(CustomerId::getId).collect(Collectors.toList())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 3687c01d02..698eb159bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.dashboard; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -83,6 +82,6 @@ public interface DashboardInfoDao extends Dao, ImageContainerDao< String findTitleById(UUID tenantId, UUID dashboardId); - ListenableFuture> findDashboardsByIdsAsync(UUID tenantId, List dashboardIds); + List findDashboardsByIds(UUID tenantId, List dashboardIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 7838c1a717..ae2186243d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -65,7 +65,6 @@ import java.util.Optional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("DashboardDaoService") @Slf4j @@ -412,10 +411,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - public ListenableFuture> findDashboardInfoByIdsAsync(TenantId tenantId, List dashboardIds) { - log.trace("Executing findDashboardInfoByIdsAsync, dashboardIds [{}]", dashboardIds); - validateIds(dashboardIds, ids -> "Incorrect dashboardIds " + ids); - return dashboardInfoDao.findDashboardsByIdsAsync(tenantId.getId(), toUUIDs(dashboardIds)); + public List findDashboardInfoByIds(TenantId tenantId, List dashboardIds) { + log.trace("Executing findDashboardInfoByIds, dashboardIds [{}]", dashboardIds); + return dashboardInfoDao.findDashboardsByIds(tenantId.getId(), toUUIDs(dashboardIds)); } private final PaginatedRemover tenantDashboardsRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index 470881a690..699d8fe595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.device; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityInfo; @@ -54,6 +53,6 @@ public interface DeviceProfileDao extends Dao, ExportableEntityDa List findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly); - ListenableFuture> findDeviceProfilesByTenantIdAndIdsAsync(UUID tenantId, List deviceProfileIds); + List findDeviceProfilesByTenantIdAndIds(UUID tenantId, List deviceProfileIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index eddf18e3c2..612be215e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.device; import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -69,7 +68,6 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validateString; @Service("DeviceProfileDaoService") @@ -395,11 +393,9 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService> findDeviceProfilesByIdsAsync(TenantId tenantId, List deviceProfileIds) { - log.trace("Executing findDeviceProfilesByIdsAsync, tenantId [{}], deviceProfileIds [{}]", tenantId, deviceProfileIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(deviceProfileIds, ids -> "Incorrect deviceProfileIds " + ids); - return deviceProfileDao.findDeviceProfilesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceProfileIds)); + public List findDeviceProfilesByIds(TenantId tenantId, List deviceProfileIds) { + log.trace("Executing findDeviceProfilesByIds, tenantId [{}], deviceProfileIds [{}]", tenantId, deviceProfileIds); + return deviceProfileDao.findDeviceProfilesByTenantIdAndIds(tenantId.getId(), toUUIDs(deviceProfileIds)); } private final PaginatedRemover tenantDeviceProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index 52d077a3bb..58fc740bd7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -169,7 +169,7 @@ public interface EntityViewDao extends Dao, ExportableEntityDao> findEntityViewsByTenantIdAndIdsAsync(UUID tenantId, List entityViewIds); + List findEntityViewsByTenantIdAndIds(UUID tenantId, List entityViewIds); /** * Find entity views by tenantId, edgeId, type and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index e135555659..43bdd515fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -64,7 +64,6 @@ import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -258,11 +257,9 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService> findEntityViewsByTenantIdAndIdsAsync(TenantId tenantId, List entityViewIds) { - log.trace("Executing findEntityViewsByTenantIdAndIdsAsync, tenantId [{}], entityViewIds [{}]", tenantId, entityViewIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(entityViewIds, ids -> "Incorrect entityViewIds " + ids); - return entityViewDao.findEntityViewsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(entityViewIds)); + public List findEntityViewsByTenantIdAndIds(TenantId tenantId, List entityViewIds) { + log.trace("Executing findEntityViewsByTenantIdAndIds, tenantId [{}], entityViewIds [{}]", tenantId, entityViewIds); + return entityViewDao.findEntityViewsByTenantIdAndIds(tenantId.getId(), toUUIDs(entityViewIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index c1561084b9..23e0b4b30d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -871,11 +871,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public ListenableFuture> findRuleChainsByIdsAsync(TenantId tenantId, List ruleChainIds) { - log.trace("Executing findRuleChainsByIdsAsync, tenantId [{}], ruleChainIds [{}]", tenantId, ruleChainIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(ruleChainIds, ids -> "Incorrect ruleChainIds " + ids); - return ruleChainDao.findRuleChainsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(ruleChainIds)); + public List findRuleChainsByIds(TenantId tenantId, List ruleChainIds) { + log.trace("Executing findRuleChainsByIds, tenantId [{}], ruleChainIds [{}]", tenantId, ruleChainIds); + return ruleChainDao.findRuleChainsByTenantIdAndIds(tenantId.getId(), toUUIDs(ruleChainIds)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 7b4e9a7fb9..b8c95656f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,14 +15,12 @@ */ package org.thingsboard.server.dao.rule; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; -import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ResourceContainerDao; @@ -85,6 +83,6 @@ public interface RuleChainDao extends Dao, TenantEntityDao Collection findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name); - ListenableFuture> findRuleChainsByTenantIdAndIdsAsync(UUID tenantId, List ruleChainIds); + List findRuleChainsByTenantIdAndIds(UUID tenantId, List ruleChainIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index 99093827e5..8ea5078b9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.asset; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.domain.PageRequest; @@ -107,8 +106,8 @@ public class JpaAssetProfileDao extends JpaAbstractDao> findAssetProfilesByTenantIdAndIdsAsync(UUID tenantId, List assetProfileIds) { - return service.submit(() -> assetProfileRepository.findAssetProfileInfosByTenantIdAndIdIn(tenantId, assetProfileIds)); + public List findAssetProfilesByTenantIdAndIds(UUID tenantId, List assetProfileIds) { + return assetProfileRepository.findAssetProfileInfosByTenantIdAndIdIn(tenantId, assetProfileIds); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index d1a31faaaf..d358f887cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.customer; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -109,8 +108,8 @@ public class JpaCustomerDao extends JpaAbstractDao imp } @Override - public ListenableFuture> findCustomersByTenantIdAndIdsAsync(UUID tenantId, List customerIds) { - return service.submit(() -> DaoUtil.convertDataList(customerRepository.findCustomersByTenantIdAndIdIn(tenantId, customerIds))); + public List findCustomersByTenantIdAndIds(UUID tenantId, List customerIds) { + return DaoUtil.convertDataList(customerRepository.findCustomersByTenantIdAndIdIn(tenantId, customerIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index f3999a10c6..f0c164d9d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -128,8 +128,8 @@ public class JpaDashboardInfoDao extends JpaAbstractDao> findDashboardsByIdsAsync(UUID tenantId, List dashboardIds) { - return service.submit(() -> DaoUtil.convertDataList(dashboardInfoRepository.findByIdIn(dashboardIds))); + public List findDashboardsByIds(UUID tenantId, List dashboardIds) { + return DaoUtil.convertDataList(dashboardInfoRepository.findByIdIn(dashboardIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 41dbf3978e..732b44efc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.device; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.domain.PageRequest; @@ -125,8 +124,8 @@ public class JpaDeviceProfileDao extends JpaAbstractDao> findDeviceProfilesByTenantIdAndIdsAsync(UUID tenantId, List deviceProfileIds) { - return service.submit(() -> deviceProfileRepository.findDeviceProfileInfosByTenantIdAndIdIn(tenantId, deviceProfileIds)); + public List findDeviceProfilesByTenantIdAndIds(UUID tenantId, List deviceProfileIds) { + return deviceProfileRepository.findDeviceProfileInfosByTenantIdAndIdIn(tenantId, deviceProfileIds); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index fe3f63825f..b03c9a4dcf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -188,8 +188,8 @@ public class JpaEntityViewDao extends JpaAbstractDao> findEntityViewsByTenantIdAndIdsAsync(UUID tenantId, List entityViewIds) { - return service.submit(() -> DaoUtil.convertDataList(entityViewRepository.findEntityViewsByTenantIdAndIdIn(tenantId, entityViewIds))); + public List findEntityViewsByTenantIdAndIds(UUID tenantId, List entityViewIds) { + return DaoUtil.convertDataList(entityViewRepository.findEntityViewsByTenantIdAndIdIn(tenantId, entityViewIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index f541627309..43db265d5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.rule; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; @@ -114,8 +113,8 @@ public class JpaRuleChainDao extends JpaAbstractDao } @Override - public ListenableFuture> findRuleChainsByTenantIdAndIdsAsync(UUID tenantId, List ruleChainIds) { - return service.submit(() -> DaoUtil.convertDataList(ruleChainRepository.findRuleChainsByTenantIdAndIdIn(tenantId, ruleChainIds))); + public List findRuleChainsByTenantIdAndIds(UUID tenantId, List ruleChainIds) { + return DaoUtil.convertDataList(ruleChainRepository.findRuleChainsByTenantIdAndIdIn(tenantId, ruleChainIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index 781753bb7d..94597bbfcb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.tenant; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -100,8 +99,8 @@ public class JpaTenantDao extends JpaAbstractDao implement } @Override - public ListenableFuture> findTenantsByIdsAsync(UUID tenantId, List tenantIds) { - return service.submit(() -> DaoUtil.convertDataList(tenantRepository.findTenantsByIdIn(tenantIds))); + public List findTenantsByIds(UUID tenantId, List tenantIds) { + return DaoUtil.convertDataList(tenantRepository.findTenantsByIdIn(tenantIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 3b3f4dc15c..576bae8da4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.user; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -143,8 +142,8 @@ public class JpaUserDao extends JpaAbstractDao implements User } @Override - public ListenableFuture> findUsersByTenantIdAndIdsAsync(UUID tenantId, List userIds) { - return service.submit(() -> DaoUtil.convertDataList(userRepository.findUsersByTenantIdAndIdIn(tenantId, userIds))); + public List findUsersByTenantIdAndIds(UUID tenantId, List userIds) { + return DaoUtil.convertDataList(userRepository.findUsersByTenantIdAndIdIn(tenantId, userIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index b1481d636a..aa4ce77fec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.widget; -import com.google.common.util.concurrent.ListenableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; @@ -113,14 +112,8 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao> findSystemWidgetBundlesByIdsAsync(UUID tenantId, List widgetsBundleIds) { - return service.submit(() -> DaoUtil.convertDataList(widgetsBundleRepository.findSystemWidgetsBundlesByIdIn(NULL_UUID, widgetsBundleIds))); - } - - @Override - public ListenableFuture> findAllTenantWidgetBundlesByTenantIdAndIdsAsync(UUID tenantId, List widgetsBundleIds) { - return service.submit(() -> DaoUtil.convertDataList(widgetsBundleRepository - .findAllTenantWidgetsBundlesByTenantIdAndIdIn(tenantId, NULL_UUID, widgetsBundleIds))); + public List findSystemOrTenantWidgetBundlesByIds(UUID tenantId, List widgetsBundleIds) { + return DaoUtil.convertDataList(widgetsBundleRepository.findSystemOrTenantWidgetsBundlesByIdIn(tenantId, TenantId.NULL_UUID, widgetsBundleIds)); } private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 53053eb3c0..1479f29e71 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -21,7 +21,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields; import org.thingsboard.server.common.data.edqs.fields.WidgetsBundleFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; @@ -147,15 +146,10 @@ public interface WidgetsBundleRepository extends JpaRepository :id ORDER BY w.id") List findNextBatch(@Param("id") UUID id, Limit limit); - @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :systemTenantId " + - "AND wb.id IN :widgetsBundleIds") - List findSystemWidgetsBundlesByIdIn(@Param("systemTenantId") UUID systemTenantId, - @Param("widgetsBundleIds") List widgetsBundleIds); - - @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId IN (:tenantId, :nullTenantId) " + - "AND wb.id IN :widgetsBundleIds") - List findAllTenantWidgetsBundlesByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, - @Param("nullTenantId") UUID nullTenantId, - @Param("widgetsBundleIds") List widgetsBundleIds); + @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE " + + "wb.id IN (:widgetsBundleIds) AND (wb.tenantId = :tenantId OR wb.tenantId = :systemTenantId)") + List findSystemOrTenantWidgetsBundlesByIdIn(@Param("tenantId") UUID tenantId, + @Param("systemTenantId") UUID systemTenantId, + @Param("widgetsBundleIds") List widgetsBundleIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index 5bab7e6295..94ea13e34e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.tenant; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; @@ -43,6 +42,6 @@ public interface TenantDao extends Dao { Tenant findTenantByName(TenantId tenantId, String name); - ListenableFuture> findTenantsByIdsAsync(UUID tenantId, List tenantIds); + List findTenantsByIds(UUID tenantId, List tenantIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index a6f8bac498..b2c74a01e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -227,10 +227,9 @@ public class TenantServiceImpl extends AbstractCachedEntityService> findTenantsByIdsAsync(TenantId callerId, List tenantIds) { - log.trace("Executing findTenantsByIdsAsync, callerId [{}], tenantIds [{}]", callerId, tenantIds); - validateIds(tenantIds, ids -> "Incorrect tenantIds " + ids); - return tenantDao.findTenantsByIdsAsync(callerId.getId(), toUUIDs(tenantIds)); + public List findTenantsByIds(TenantId callerId, List tenantIds) { + log.trace("Executing findTenantsByIds, callerId [{}], tenantIds [{}]", callerId, tenantIds); + return tenantDao.findTenantsByIds(callerId.getId(), toUUIDs(tenantIds)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index 26e98a4edf..99b9808d08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.user; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -104,6 +103,6 @@ public interface UserDao extends Dao, TenantEntityDao { int countTenantAdmins(UUID tenantId); - ListenableFuture> findUsersByTenantIdAndIdsAsync(UUID tenantId, List userIds); + List findUsersByTenantIdAndIds(UUID tenantId, List userIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index dbf3c72c5b..a8d3a418af 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -77,7 +77,6 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -495,11 +494,9 @@ public class UserServiceImpl extends AbstractCachedEntityService> findUsersByTenantIdAndIdsAsync(TenantId tenantId, List userIds) { - log.trace("Executing findUsersByTenantIdAndIdsAsync, tenantId [{}], userIds [{}]", tenantId, userIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(userIds, ids -> "Incorrect userIds " + ids); - return userDao.findUsersByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(userIds)); + public List findUsersByTenantIdAndIds(TenantId tenantId, List userIds) { + log.trace("Executing findUsersByTenantIdAndIds, tenantId [{}], userIds [{}]", tenantId, userIds); + return userDao.findUsersByTenantIdAndIds(tenantId.getId(), toUUIDs(userIds)); } private Optional findMobileSessionInfo(TenantId tenantId, UserId userId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index b9e03072b1..562cdc29c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.widget; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; @@ -89,9 +88,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa PageData findAllWidgetsBundles(PageLink pageLink); - ListenableFuture> findSystemWidgetBundlesByIdsAsync(UUID tenantId, List widgetsBundleIds); - - ListenableFuture> findAllTenantWidgetBundlesByTenantIdAndIdsAsync(UUID tenantId, List widgetsBundleIds); + List findSystemOrTenantWidgetBundlesByIds(UUID tenantId, List widgetsBundleIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 5d4b6d4bb0..a98e9b78eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.widget; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -51,8 +50,6 @@ import java.util.stream.Stream; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; -import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("WidgetsBundleDaoService") @Slf4j @@ -257,19 +254,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } @Override - public ListenableFuture> findSystemWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds) { - log.trace("Executing findSystemWidgetsBundlesByIdsAsync, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(widgetsBundleIds, ids -> "Incorrect widgetsBundleIds " + ids); - return widgetsBundleDao.findSystemWidgetBundlesByIdsAsync(tenantId.getId(), toUUIDs(widgetsBundleIds)); - } - - @Override - public ListenableFuture> findAllTenantWidgetsBundlesByIdsAsync(TenantId tenantId, List widgetsBundleIds) { - log.trace("Executing findAllTenantWidgetsBundlesByIdsAsync, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateIds(widgetsBundleIds, ids -> "Incorrect widgetsBundleIds " + ids); - return widgetsBundleDao.findAllTenantWidgetBundlesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(widgetsBundleIds)); + public List findSystemOrTenantWidgetsBundlesByIds(TenantId tenantId, List widgetsBundleIds) { + log.trace("Executing findSystemOrTenantWidgetsBundlesByIds, tenantId [{}], widgetsBundleIds [{}]", tenantId, widgetsBundleIds); + return widgetsBundleDao.findSystemOrTenantWidgetBundlesByIds(tenantId.getId(), toUUIDs(widgetsBundleIds)); } private WidgetTypeDetails updateSystemWidget(JsonNode widgetTypeJson) { From 607287b3ac815d472a5b24e5e824660139e6f365 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Dec 2025 16:29:31 +0200 Subject: [PATCH 028/147] moved check for intermediate result to hasRefreshContextOnlyChanges --- .../cf/ctx/state/CalculatedFieldCtx.java | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index f6c577545e..d2bc8d30cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -639,26 +639,34 @@ public class CalculatedFieldCtx implements Closeable { } public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation - var thisConfig = calculatedField.getConfiguration(); - var otherConfig = other.getCalculatedField().getConfiguration(); - - var thisOutputStrategy = thisConfig.getOutput().getStrategy(); - var otherOutputStrategy = otherConfig.getOutput().getStrategy(); + var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy(); + var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { return true; } - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { - return thisTimeSeriesImmediateOutputStrategy.getTtl() != otherTimeSeriesImmediateOutputStrategy.getTtl(); + if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTsOutputStrategy + && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTsOutputStrategy) { + if (thisTsOutputStrategy.getTtl() != otherTsOutputStrategy.getTtl()) { + return true; + } } - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { - boolean updateAttributesOnlyOnValueChangeChanged = thisAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttributesImmediateOutputStrategy.isUpdateAttributesOnlyOnValueChange(); - boolean sendAttributesUpdatedNotificationUpdated = thisAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification() != otherAttributesImmediateOutputStrategy.isSendAttributesUpdatedNotification(); - return updateAttributesOnlyOnValueChangeChanged || sendAttributesUpdatedNotificationUpdated; + if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttrOutputStrategy + && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttrOutputStrategy) { + boolean updateAttrOnValueChangedChanged = thisAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttrUpdatedNotificationChanged = thisAttrOutputStrategy.isSendAttributesUpdatedNotification() != otherAttrOutputStrategy.isSendAttributesUpdatedNotification(); + if (updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged) { + return true; + } + } + + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) { + return true; + } } return false; @@ -705,8 +713,9 @@ public class CalculatedFieldCtx implements Closeable { && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); - boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult(); - return metricsChanged || watermarkChanged || produceIntermediateResultChanged; + if (metricsChanged || watermarkChanged) { + return true; + } } return false; } @@ -741,7 +750,7 @@ public class CalculatedFieldCtx implements Closeable { if (!output.getType().equals(otherOutput.getType())) { return true; } - if (!output.getName().equals(otherOutput.getName())) { + if (!Objects.equals(output.getName(), otherOutput.getName())) { return true; } if (output.getScope() != (otherOutput.getScope())) { From ba19ebd6acf86e749c5da1fbe2156b0683e3a731 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:11:57 +0200 Subject: [PATCH 029/147] added debug events modal to rule chain elements --- .../rule-node-debug-dialog.component.html | 47 ++++++++++++++ .../rule-node-debug-dialog.component.scss | 26 ++++++++ .../rule-node-debug-dialog.component.ts | 65 +++++++++++++++++++ .../rule-node-details.component.html | 1 + .../rulechain/rule-node-details.component.ts | 4 ++ .../rulechain/rulechain-page.component.html | 1 + .../rulechain/rulechain-page.component.ts | 35 +++++++++- .../pages/rulechain/rulechain-page.module.ts | 4 +- 8 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html new file mode 100644 index 0000000000..b2bd40aa1a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html @@ -0,0 +1,47 @@ + +
+ +

{{ 'rulenode.events' | translate}}

+ + +
+
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss new file mode 100644 index 0000000000..c4798d06f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .debug-dialog-container { + width: 1080px; + max-width: 100%; + + .debug-dialog-content { + height: 65vh; + border-radius: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts new file mode 100644 index 0000000000..e7a7947009 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { EventTableComponent } from '@home/components/event/event-table.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DebugEventType, EventType } from '@shared/models/event.models'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { EntityId } from '@shared/models/id/entity-id'; + +export interface RuleNodeDebugDialogData { + isActive: boolean; + tenantId: TenantId; + ruleNodeId: EntityId; + ruleNodeTestButtonLabel: string; + onDebugEventSelected: ()=> void; +} + +@Component({ + selector: 'tb-rule-node-debug-dialog', + templateUrl: './rule-node-debug-dialog.component.html', + styleUrl: './rule-node-debug-dialog.component.scss' +}) +export class RuleNodeDebugDialogComponent extends DialogComponent implements AfterViewInit{ + + @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; + + readonly DebugEventType = DebugEventType; + readonly debugEventTypes = DebugEventType; + readonly EventType = EventType; + + dialogTitle: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, + protected dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngAfterViewInit() { + this.eventsTable.entitiesTable.updateData(); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index b55dcdaa73..a66819a49b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -36,6 +36,7 @@
(); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 423bde7da3..c31f24c0ea 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -106,6 +106,7 @@ (RuleNodeDebugDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + active: true, + tenantId: this.ruleChain.tenantId, + ruleNodeId: this.editingRuleNode.ruleNodeId, + functionTestButtonLabel: this.ruleNodeTestButtonLabel, + debugEventSelected: this.onDebugEventSelected.bind(this) + } + }) + .afterClosed() + .subscribe(); + } + private updateNodeErrorTooltip(node: FcRuleNode) { if (node.error) { const element = $('#' + node.id); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts index 34518e2b70..51197e1073 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts @@ -33,6 +33,7 @@ import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-detail import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.component'; +import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; @NgModule({ declarations: [ @@ -43,7 +44,8 @@ import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.comp RuleNodeConfigComponent, AddRuleNodeLinkDialogComponent, AddRuleNodeDialogComponent, - CreateNestedRuleChainDialogComponent + CreateNestedRuleChainDialogComponent, + RuleNodeDebugDialogComponent ], imports: [ CommonModule, From 185ef9b53b31de8632d5d34105ada22b3fcbe6d4 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:46:27 +0200 Subject: [PATCH 030/147] move events modal to reusable component --- .../event/events-dialog.component.html} | 8 ++++---- .../event/events-dialog.component.scss} | 0 .../event/events-dialog.component.ts} | 19 ++++++++----------- .../home/components/home-components.module.ts | 5 ++++- .../rulechain/rulechain-page.component.ts | 8 ++++---- .../pages/rulechain/rulechain-page.module.ts | 4 +--- 6 files changed, 21 insertions(+), 23 deletions(-) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.html => components/event/events-dialog.component.html} (89%) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.scss => components/event/events-dialog.component.scss} (100%) rename ui-ngx/src/app/modules/home/{pages/rulechain/rule-node-debug-dialog.component.ts => components/event/events-dialog.component.ts} (78%) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html similarity index 89% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.html index b2bd40aa1a..65040bcf60 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ 'rulenode.events' | translate}}

+

{{ data.title | translate}}

diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts similarity index 78% rename from ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts index e7a7947009..5a21241868 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts @@ -26,32 +26,29 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { EntityId } from '@shared/models/id/entity-id'; export interface RuleNodeDebugDialogData { - isActive: boolean; + title: string; tenantId: TenantId; - ruleNodeId: EntityId; - ruleNodeTestButtonLabel: string; + entityId: EntityId; + functionTestButtonLabel: string; onDebugEventSelected: ()=> void; } @Component({ - selector: 'tb-rule-node-debug-dialog', - templateUrl: './rule-node-debug-dialog.component.html', - styleUrl: './rule-node-debug-dialog.component.scss' + selector: 'tb-debug-dialog', + templateUrl: './events-dialog.component.html', + styleUrl: './events-dialog.component.scss' }) -export class RuleNodeDebugDialogComponent extends DialogComponent implements AfterViewInit{ +export class EventsDialogComponent extends DialogComponent implements AfterViewInit{ @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - readonly DebugEventType = DebugEventType; readonly debugEventTypes = DebugEventType; readonly EventType = EventType; - dialogTitle: string; - constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, - protected dialogRef: MatDialogRef) { + protected dialogRef: MatDialogRef) { super(store, router, dialogRef); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 6da26fb2c3..8a90c28878 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -204,6 +204,7 @@ import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/ed import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; +import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; @NgModule({ declarations: @@ -361,7 +362,8 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi EditApiKeyDescriptionPanelComponent, ApiKeyGeneratedDialogComponent, AuditLogHeaderComponent, - AuditLogFilterComponent + AuditLogFilterComponent, + EventsDialogComponent ], imports: [ CommonModule, @@ -510,6 +512,7 @@ import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-fi ResourcesLibraryComponent, ApiKeysTableComponent, ApiKeysTableDialogComponent, + EventsDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 53e2563ee2..3123719f22 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -100,7 +100,7 @@ import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; import { DomSanitizer } from '@angular/platform-browser'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; +import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; @Component({ selector: 'tb-rulechain-page', @@ -1674,13 +1674,13 @@ export class RuleChainPageComponent extends PageComponent } private openDebugEventsDialog(): void { - this.dialog.open(RuleNodeDebugDialogComponent, { + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - active: true, + title: 'rulenode.events', tenantId: this.ruleChain.tenantId, - ruleNodeId: this.editingRuleNode.ruleNodeId, + entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, debugEventSelected: this.onDebugEventSelected.bind(this) } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts index 51197e1073..34518e2b70 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.module.ts @@ -33,7 +33,6 @@ import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-detail import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; import { RuleNodeLinkComponent } from '@home/pages/rulechain/rule-node-link.component'; -import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-debug-dialog.component'; @NgModule({ declarations: [ @@ -44,8 +43,7 @@ import { RuleNodeDebugDialogComponent } from '@home/pages/rulechain/rule-node-de RuleNodeConfigComponent, AddRuleNodeLinkDialogComponent, AddRuleNodeDialogComponent, - CreateNestedRuleChainDialogComponent, - RuleNodeDebugDialogComponent + CreateNestedRuleChainDialogComponent ], imports: [ CommonModule, From 0d66c6b33e72877270a5aece61e850ac5c4e396b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 12:51:08 +0200 Subject: [PATCH 031/147] adjustments --- .../home/components/event/events-dialog.component.html | 4 ++-- .../home/components/event/events-dialog.component.ts | 6 ++---- .../home/pages/rulechain/rulechain-page.component.ts | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html index 65040bcf60..7aa1f3dd7f 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html @@ -27,8 +27,8 @@
void; } @@ -42,9 +43,6 @@ export class EventsDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 3123719f22..163b4ff1f6 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1679,6 +1679,7 @@ export class RuleChainPageComponent extends PageComponent panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'rulenode.events', + debugEventType: DebugEventType.DEBUG_RULE_CHAIN, tenantId: this.ruleChain.tenantId, entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, From 30e3c447f37d6072a8d445aae5c9fa76cc6bfa22 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 8 Dec 2025 15:03:13 +0200 Subject: [PATCH 032/147] moved calculated-fields-table to use debug event modal --- .../alarm-rules/alarm-rules-table-config.ts | 24 ++++-- .../calculated-fields-table-config.ts | 29 +++++-- ...lculated-field-debug-dialog.component.html | 47 ----------- ...lculated-field-debug-dialog.component.scss | 26 ------- ...calculated-field-debug-dialog.component.ts | 77 ------------------- .../components/public-api.ts | 1 - .../home/components/home-components.module.ts | 6 +- .../events-dialog.component.html | 13 ++-- .../events-dialog.component.scss | 0 .../events-dialog.component.ts | 30 +++++--- .../rulechain/rulechain-page.component.ts | 11 +-- 11 files changed, 75 insertions(+), 189 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.html (79%) rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.scss (100%) rename ui-ngx/src/app/modules/home/{components/event => dialogs}/events-dialog.component.ts (64%) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index f73633ad8e..15c7cdaeb1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -55,14 +55,12 @@ import { AlarmRuleDialogComponent, AlarmRuleDialogData } from "@home/components/alarm-rules/alarm-rule-dialog.component"; -import { - CalculatedFieldDebugDialogComponent, - CalculatedFieldDebugDialogData -} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; import { UtilsService } from "@core/services/utils.service"; import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { ActionNotificationShow } from "@core/notification/notification.actions"; import { CalculatedFieldScriptTestDialogComponent, @@ -266,13 +264,25 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private openDebugEventsDialog(calculatedField: CalculatedField): void { - this.dialog.open(CalculatedFieldDebugDialogComponent, { + const customCellActionEnabledFn = (event: DebugEvent) => { + return (calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && + calculatedField.configuration.applyExpressionToResolvedArguments) + ) && !!(event as DebugEvent).body.arguments; + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: null, + value: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected: null, + customCellActionEnabledFn: customCellActionEnabledFn } }) .afterClosed() diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 98e4a29549..f024e1c88c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -22,7 +22,7 @@ import { import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { PageLink } from '@shared/models/page/page-link'; import { EMPTY, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; @@ -48,8 +48,6 @@ import { PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, - CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, @@ -61,6 +59,8 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; +import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -233,13 +233,30 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig(CalculatedFieldDebugDialogComponent, { + const customCellActionEnabledFn = (event: DebugEvent) => { + return (calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && + calculatedField.configuration.applyExpressionToResolvedArguments) + ) && !!(event as DebugEvent).body.arguments; + }; + + const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) + .subscribe(expression => dialogRef.close(expression)); + }; + + this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { + title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField, - getTestScriptDialogFn: this.getTestScriptDialog.bind(this), + value: calculatedField.id, + debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], + disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], + defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, + onDebugEventSelected: onDebugEventSelected, + customCellActionEnabledFn: customCellActionEnabledFn } }) .afterClosed() diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html deleted file mode 100644 index 168657b8cb..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html +++ /dev/null @@ -1,47 +0,0 @@ - -
- -

{{ dialogTitle | translate}}

- - -
-
- -
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss deleted file mode 100644 index c4798d06f9..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - .debug-dialog-container { - width: 1080px; - max-width: 100%; - - .debug-dialog-content { - height: 65vh; - border-radius: 0; - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts deleted file mode 100644 index 70bfa43c63..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -/// -/// Copyright © 2016-2025 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { Router } from '@angular/router'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models'; -import { EventTableComponent } from '@home/components/event/event-table.component'; -import { - CalculatedField, - CalculatedFieldTestScriptFn, - CalculatedFieldType -} from '@shared/models/calculated-field.models'; - -export interface CalculatedFieldDebugDialogData { - tenantId: string; - value: CalculatedField; - getTestScriptDialogFn: CalculatedFieldTestScriptFn; -} - -@Component({ - selector: 'tb-calculated-field-debug-dialog', - styleUrls: ['calculated-field-debug-dialog.component.scss'], - templateUrl: './calculated-field-debug-dialog.component.html', -}) -export class CalculatedFieldDebugDialogComponent extends DialogComponent implements AfterViewInit { - - @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; - - readonly DebugEventType = DebugEventType; - readonly debugEventTypes = DebugEventType; - readonly EventType = EventType; - - dialogTitle: string; - - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, - protected dialogRef: MatDialogRef) { - super(store, router, dialogRef); - this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging'; - } - - ngAfterViewInit(): void { - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => { - return (this.data.value.type === CalculatedFieldType.SCRIPT || - (this.data.value.type === CalculatedFieldType.PROPAGATION && this.data.value.configuration.applyExpressionToResolvedArguments) - ) && !!(event as Event).body.arguments - }); - this.eventsTable.entitiesTable.updateData(); - } - - cancel(): void { - this.dialogRef.close(null); - } - - onDebugEventSelected(event: CalculatedFieldEventBody): void { - this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments)) - .subscribe(expression => this.dialogRef.close(expression)); - } -} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index d4d4f9d1da..f114fd4d67 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -15,5 +15,4 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 8a90c28878..b159b9240c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -192,9 +192,6 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; -import { - CalculatedFieldDebugDialogComponent -} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module'; import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module"; import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; @@ -204,7 +201,7 @@ import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/ed import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; -import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @NgModule({ declarations: @@ -218,7 +215,6 @@ import { EventsDialogComponent } from '@home/components/event/events-dialog.comp AuditLogTableComponent, AuditLogDetailsDialogComponent, CalculatedFieldsTableComponent, - CalculatedFieldDebugDialogComponent, AlarmRulesTableComponent, EventContentDialogComponent, EventTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html similarity index 79% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.html rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html index 7aa1f3dd7f..5197f6bb4e 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html @@ -27,13 +27,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.scss rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.scss diff --git a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts similarity index 64% rename from ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts rename to ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts index 30e8a0f4f1..613f4e9c15 100644 --- a/ui-ngx/src/app/modules/home/components/event/events-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts @@ -21,17 +21,20 @@ import { EventTableComponent } from '@home/components/event/event-table.componen import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; -import { DebugEventType } from '@shared/models/event.models'; -import { TenantId } from '@shared/models/id/tenant-id'; +import { DebugEventType, EventType } from '@shared/models/event.models'; import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData, HasId } from '@shared/models/base-data'; -export interface RuleNodeDebugDialogData { +export interface EventsDialogData { title: string; - tenantId: TenantId; - entityId: EntityId; - debugEventType: DebugEventType; - functionTestButtonLabel: string; - onDebugEventSelected: ()=> void; + tenantId: string; + value: EntityId; + debugEventTypes: Array; + defaultEventType: DebugEventType; + disabledEventTypes?: Array; + functionTestButtonLabel?: string; + onDebugEventSelected?: (event: any, dialogRef: MatDialogRef) => void; + customCellActionEnabledFn?: (event: BaseData) => boolean; } @Component({ @@ -45,16 +48,25 @@ export class EventsDialogComponent extends DialogComponent, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: RuleNodeDebugDialogData, + @Inject(MAT_DIALOG_DATA) public data: EventsDialogData, protected dialogRef: MatDialogRef) { super(store, router, dialogRef); } ngAfterViewInit() { + if (this.data.customCellActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) { + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.customCellActionEnabledFn; + } this.eventsTable.entitiesTable.updateData(); } cancel(): void { this.dialogRef.close(null); } + + onDebugEventSelected(event: any): void { + if(this.data.onDebugEventSelected) { + this.data.onDebugEventSelected(event, this.dialogRef); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 163b4ff1f6..aa2b4fd3eb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -100,7 +100,7 @@ import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; import { DomSanitizer } from '@angular/platform-browser'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { EventsDialogComponent } from '@home/components/event/events-dialog.component'; +import { EventsDialogComponent } from '@home/dialogs/events-dialog.component'; @Component({ selector: 'tb-rulechain-page', @@ -1679,11 +1679,12 @@ export class RuleChainPageComponent extends PageComponent panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'rulenode.events', - debugEventType: DebugEventType.DEBUG_RULE_CHAIN, - tenantId: this.ruleChain.tenantId, - entityId: this.editingRuleNode.ruleNodeId, + debugEventTypes: [DebugEventType.DEBUG_RULE_CHAIN], + defaultEventType: DebugEventType.DEBUG_RULE_CHAIN, + tenantId: this.ruleChain.tenantId.id, + value: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, - debugEventSelected: this.onDebugEventSelected.bind(this) + onDebugEventSelected: this.onDebugEventSelected.bind(this) } }) .afterClosed() From ece81a14770c2028d75182a4af666a3685cd1c67 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 8 Dec 2025 17:01:22 +0200 Subject: [PATCH 033/147] moved fixed rulenode DebugEventType --- .../calculated-fields/calculated-fields-table-config.ts | 2 +- .../modules/home/pages/rulechain/rulechain-page.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index f024e1c88c..18dcb28a2e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -249,7 +249,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig Date: Mon, 8 Dec 2025 17:04:16 +0200 Subject: [PATCH 034/147] refactoring --- .../cf/ctx/state/CalculatedFieldCtx.java | 44 ++----------------- .../AttributesImmediateOutputStrategy.java | 22 ++++++++++ .../AttributesRuleChainOutputStrategy.java | 11 +++++ .../data/cf/configuration/OutputStrategy.java | 6 +++ .../TimeSeriesImmediateOutputStrategy.java | 21 +++++++++ .../TimeSeriesRuleChainOutputStrategy.java | 11 +++++ 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index d2bc8d30cf..7e5d666ca5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -37,14 +37,12 @@ import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldC import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; @@ -641,27 +639,10 @@ public class CalculatedFieldCtx implements Closeable { public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy(); var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); - - if (!thisOutputStrategy.getType().equals(otherOutputStrategy.getType())) { + if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) { return true; } - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTsOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTsOutputStrategy) { - if (thisTsOutputStrategy.getTtl() != otherTsOutputStrategy.getTtl()) { - return true; - } - } - - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttrOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttrOutputStrategy) { - boolean updateAttrOnValueChangedChanged = thisAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange() != otherAttrOutputStrategy.isUpdateAttributesOnlyOnValueChange(); - boolean sendAttrUpdatedNotificationChanged = thisAttrOutputStrategy.isSendAttributesUpdatedNotification() != otherAttrOutputStrategy.isSendAttributesUpdatedNotification(); - if (updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged) { - return true; - } - } - if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) { @@ -759,28 +740,9 @@ public class CalculatedFieldCtx implements Closeable { if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { return true; } - - var thisOutputStrategy = output.getStrategy(); - var otherOutputStrategy = otherOutput.getStrategy(); - - if (thisOutputStrategy instanceof TimeSeriesImmediateOutputStrategy thisTimeSeriesImmediateOutputStrategy - && otherOutputStrategy instanceof TimeSeriesImmediateOutputStrategy otherTimeSeriesImmediateOutputStrategy) { - boolean saveTimeSeriesUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveTimeSeries() != otherTimeSeriesImmediateOutputStrategy.isSaveTimeSeries(); - boolean saveLatestUpdated = thisTimeSeriesImmediateOutputStrategy.isSaveLatest() != otherTimeSeriesImmediateOutputStrategy.isSaveLatest(); - boolean sendWsUpdateUpdated = thisTimeSeriesImmediateOutputStrategy.isSendWsUpdate() != otherTimeSeriesImmediateOutputStrategy.isSendWsUpdate(); - boolean processCfsUpdated = thisTimeSeriesImmediateOutputStrategy.isProcessCfs() != otherTimeSeriesImmediateOutputStrategy.isProcessCfs(); - return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; - } - - if (thisOutputStrategy instanceof AttributesImmediateOutputStrategy thisAttributesImmediateOutputStrategy - && otherOutputStrategy instanceof AttributesImmediateOutputStrategy otherAttributesImmediateOutputStrategy) { - - boolean saveTimeSeriesUpdated = thisAttributesImmediateOutputStrategy.isSaveAttribute() != otherAttributesImmediateOutputStrategy.isSaveAttribute(); - boolean sendWsUpdateUpdated = thisAttributesImmediateOutputStrategy.isSendWsUpdate() != otherAttributesImmediateOutputStrategy.isSendWsUpdate(); - boolean processCfsUpdated = thisAttributesImmediateOutputStrategy.isProcessCfs() != otherAttributesImmediateOutputStrategy.isProcessCfs(); - return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + if (output.getStrategy().hasContextOnlyChanges(otherOutput.getStrategy())) { + return true; } - return false; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java index 714180930d..81874002cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java @@ -35,4 +35,26 @@ public class AttributesImmediateOutputStrategy implements AttributesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.IMMEDIATE; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveAttribute != otherStrategy.isSaveAttribute(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof AttributesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean updateAttrOnValueChangedChanged = updateAttributesOnlyOnValueChange != otherStrategy.isUpdateAttributesOnlyOnValueChange(); + boolean sendAttrUpdatedNotificationChanged = sendAttributesUpdatedNotification != otherStrategy.isSendAttributesUpdatedNotification(); + return updateAttrOnValueChangedChanged || sendAttrUpdatedNotificationChanged; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java index 1a3348ce74..f39efadb86 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java @@ -26,4 +26,15 @@ public class AttributesRuleChainOutputStrategy implements AttributesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.RULE_CHAIN; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof AttributesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index b635047ae3..252af82ef7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -22,4 +22,10 @@ public interface OutputStrategy { @JsonIgnore OutputStrategyType getType(); + @JsonIgnore + boolean hasContextOnlyChanges(OutputStrategy other); + + @JsonIgnore + boolean hasRefreshContextOnlyChanges(OutputStrategy other); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java index 31b15095d4..a24bfc8683 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -35,4 +35,25 @@ public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.IMMEDIATE; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + boolean saveTimeSeriesUpdated = saveTimeSeries != otherStrategy.isSaveTimeSeries(); + boolean saveLatestUpdated = saveLatest != otherStrategy.isSaveLatest(); + boolean sendWsUpdateUpdated = sendWsUpdate != otherStrategy.isSendWsUpdate(); + boolean processCfsUpdated = processCfs != otherStrategy.isProcessCfs(); + return saveTimeSeriesUpdated || saveLatestUpdated || sendWsUpdateUpdated || processCfsUpdated; + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + if (!(other instanceof TimeSeriesImmediateOutputStrategy otherStrategy)) { + return true; + } + return ttl != otherStrategy.getTtl(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java index 7786efff1a..0b17594869 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -26,4 +26,15 @@ public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrate public OutputStrategyType getType() { return OutputStrategyType.RULE_CHAIN; } + + @Override + public boolean hasContextOnlyChanges(OutputStrategy other) { + return !(other instanceof TimeSeriesRuleChainOutputStrategy); + } + + @Override + public boolean hasRefreshContextOnlyChanges(OutputStrategy other) { + return false; + } + } From c121b6b5e46b16fe991cdf2800d7f5cbdf8de856 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 8 Dec 2025 17:19:12 +0200 Subject: [PATCH 035/147] Update entities limit exception processing. Implement entities limit increase request notification. --- .../server/controller/BaseController.java | 3 + .../controller/NotificationController.java | 39 +++++++ .../exception/ThingsboardErrorResponse.java | 28 +++++ .../ThingsboardErrorResponseHandler.java | 24 +++++ .../install/ThingsboardInstallService.java | 1 + .../data/exception/ThingsboardErrorCode.java | 1 + .../data/notification/NotificationType.java | 1 + ...sLimitIncreaseRequestNotificationInfo.java | 50 +++++++++ .../dao/exception/EntitiesLimitException.java | 6 +- .../DefaultNotificationSettingsService.java | 4 + .../notification/DefaultNotifications.java | 38 ++++++- .../server/dao/service/DataValidator.java | 3 +- .../src/app/core/http/notification.service.ts | 5 + .../interceptors/global-http-interceptor.ts | 4 + .../src/app/core/services/dialog.service.ts | 15 +++ .../template-notification-dialog.component.ts | 1 + ...ntity-limit-exceeded-dialog.component.html | 40 +++++++ ...ntity-limit-exceeded-dialog.component.scss | 97 +++++++++++++++++ .../entity-limit-exceeded-dialog.component.ts | 100 ++++++++++++++++++ .../notification/notification.component.ts | 2 + ui-ngx/src/app/shared/models/constants.ts | 1 + .../app/shared/models/notification.models.ts | 8 ++ ui-ngx/src/app/shared/shared.module.ts | 3 + ui-ngx/src/assets/entity-limit-reached.svg | 20 ++++ .../entities_limit_increase_request.md | 46 ++++++++ .../assets/locale/locale.constant-en_US.json | 11 +- 26 files changed, 544 insertions(+), 7 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java create mode 100644 ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss create mode 100644 ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts create mode 100644 ui-ngx/src/assets/entity-limit-reached.svg create mode 100644 ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 621657c02d..a58ae911a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -151,6 +151,7 @@ import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; @@ -452,6 +453,8 @@ public abstract class BaseController { } if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; + } else if (exception instanceof EntitiesLimitException) { + return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED); } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index b536b9bb8a..1508354f19 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,6 +36,8 @@ import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationTargetId; @@ -44,6 +47,9 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.EntitiesLimitIncreaseRequestNotificationInfo; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; @@ -51,6 +57,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; @@ -69,6 +76,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.service.security.system.SystemSecurityService; import java.util.Comparator; import java.util.HashMap; @@ -76,6 +84,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -90,6 +99,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; @RestController @@ -105,6 +115,7 @@ public class NotificationController extends BaseController { private final NotificationTargetService notificationTargetService; private final NotificationCenter notificationCenter; private final NotificationSettingsService notificationSettingsService; + private final SystemSecurityService systemSecurityService; @ApiOperation(value = "Get notifications (getNotifications)", notes = "Returns the page of notifications for current user." + NEW_LINE + @@ -282,6 +293,34 @@ public class NotificationController extends BaseController { return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null)); } + @ApiOperation(value = "Send entity limit increase request notification to System administrators (sendEntitiesLimitIncreaseRequest)", + notes = "Send entity limit increase request notification by Tenant Administrator to System administrators." + + TENANT_AUTHORITY_PARAGRAPH) + @PostMapping("/notification/entitiesLimitIncreaseRequest/{entityType}") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + public void sendEntitiesLimitIncreaseRequest(@Parameter(description = "Entity type", required = true, schema = @Schema(allowableValues = {"DEVICE", "ASSET", "CUSTOMER", "USER", "DASHBOARD", "RULE_CHAIN", "EDGE"})) + @PathVariable("entityType") String strEntityType, + @AuthenticationPrincipal SecurityUser user, + HttpServletRequest request) throws Exception { + EntityType entityType = checkEnumParameter("entityType", strEntityType, EntityType::valueOf); + Optional sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(TenantId.SYS_TENANT_ID, UsersFilterType.SYSTEM_ADMINISTRATORS) + .stream().findFirst(); + if (sysAdmins.isPresent()) { + NotificationTargetId notificationTargetId = sysAdmins.get().getId(); + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + NotificationInfo info = EntitiesLimitIncreaseRequestNotificationInfo.builder() + .entityType(entityType) + .userEmail(user.getEmail()) + .increaseLimitActionLabel("Set new limit") + .increaseLimitLink("/tenantProfiles/"+tenantService.findTenantById(user.getTenantId()).getTenantProfileId().toString()) + .baseUrl(baseUrl) + .build(); + notificationCenter.sendSystemNotification(TenantId.SYS_TENANT_ID, notificationTargetId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, info); + } else { + throw new IllegalArgumentException("Notification target for 'System administrators' not found"); + } + } + @ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)", notes = "Returns preview for notification request." + NEW_LINE + "`processedTemplates` shows how the notifications for each delivery method will look like " + diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index d34ae3c89b..f38b86e394 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.exception; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @Schema @@ -32,9 +34,19 @@ public class ThingsboardErrorResponse { private final long timestamp; + private EntityType entityType; + + private Long limit; + protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, HttpStatus status) { + this(message, errorCode, null, null, status); + } + + protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, EntityType entityType, Long limit, HttpStatus status) { this.message = message; this.errorCode = errorCode; + this.entityType = entityType; + this.limit = limit; this.status = status; this.timestamp = System.currentTimeMillis(); } @@ -43,6 +55,14 @@ public class ThingsboardErrorResponse { return new ThingsboardErrorResponse(message, errorCode, status); } + public static ThingsboardErrorResponse ofEntityLimitExceeded(final String message, + EntityType entityType, + Long limit, + HttpStatus status) { + return new ThingsboardErrorResponse(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, + entityType, limit, status); + } + @Schema(description = "HTTP Response Status Code", example = "401", accessMode = Schema.AccessMode.READ_ONLY) public Integer getStatus() { return status.value(); @@ -75,4 +95,12 @@ public class ThingsboardErrorResponse { public long getTimestamp() { return timestamp; } + + public EntityType getEntityType() { + return entityType; + } + + public Long getLimit() { + return limit; + } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 56292dfa1b..e6e27e6a75 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -47,10 +47,12 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.util.WebUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; @@ -95,6 +97,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS); errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS); errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN); + errorCodeToStatusMap.put(ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); errorCodeToStatusMap.put(ThingsboardErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT); } @@ -143,6 +146,12 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand handleSubscriptionException(thingsboardException, response); } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) { handleDatabaseException(thingsboardException.getCause(), response); + } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) { + if (thingsboardException.getCause() instanceof EntitiesLimitException entitiesLimitException) { + handleEntitiesLimitException(entitiesLimitException, response); + } else { + handleEntitiesLimitException(thingsboardException, response); + } } else { handleThingsboardException(thingsboardException, response); } @@ -218,6 +227,21 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand writeResponse(errorResponse, response); } + private void handleEntitiesLimitException(ThingsboardException entitiesLimitException, HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN.value()); + JacksonUtil.writeValue(response.getWriter(), + JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitException.getCause()).getResponseBodyAsByteArray(), Object.class)); + } + + private void handleEntitiesLimitException(EntitiesLimitException entitiesLimitException, HttpServletResponse response) throws IOException { + EntityType entityType = entitiesLimitException.getEntityType(); + Long limit = entitiesLimitException.getLimit(); + HttpStatus status = HttpStatus.FORBIDDEN; + response.setStatus(status.value()); + JacksonUtil.writeValue(response.getWriter(), + ThingsboardErrorResponse.ofEntityLimitExceeded(entitiesLimitException.getMessage(), entityType, limit, status)); + } + private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 6765e95246..5e5185ac8b 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -116,6 +116,7 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createDatabaseIndexes(); // TODO: cleanup update code after each release + systemDataLoaderService.updateDefaultNotificationConfigs(false); // Runs upgrade scripts that are not possible in plain SQL. dataUpdateService.updateData(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index deae86a9de..1025e9a90e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -31,6 +31,7 @@ public enum ThingsboardErrorCode { TOO_MANY_UPDATES(34), VERSION_CONFLICT(35), SUBSCRIPTION_VIOLATION(40), + ENTITIES_LIMIT_EXCEEDED(41), PASSWORD_VIOLATION(45), DATABASE(46); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index d4d3d11e8b..005e2f64d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -32,6 +32,7 @@ public enum NotificationType { ALARM_ASSIGNMENT, NEW_PLATFORM_VERSION, ENTITIES_LIMIT, + ENTITIES_LIMIT_INCREASE_REQUEST, API_USAGE_LIMIT, RULE_NODE, RATE_LIMITS, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java new file mode 100644 index 0000000000..208b43282e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.info; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Map; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EntitiesLimitIncreaseRequestNotificationInfo implements NotificationInfo { + + private EntityType entityType; + private String userEmail; + private String increaseLimitActionLabel; + private String increaseLimitLink; + private String baseUrl; + + @Override + public Map getTemplateData() { + return mapOf( + "entityType", entityType.getNormalName(), + "userEmail", userEmail, + "increaseLimitActionLabel", increaseLimitActionLabel, + "increaseLimitLink", increaseLimitLink, + "baseUrl", baseUrl + ); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java index 64ed1d1328..559ef94e45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java @@ -27,9 +27,13 @@ public class EntitiesLimitException extends DataValidationException { @Getter private final EntityType entityType; - public EntitiesLimitException(TenantId tenantId, EntityType entityType) { + @Getter + private final long limit; + + public EntitiesLimitException(TenantId tenantId, EntityType entityType, long limit) { super(entityType.getNormalName() + "s limit reached"); this.tenantId = tenantId; this.entityType = entityType; + this.limit = limit; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index ea7d3488b2..038d24b386 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -177,6 +177,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForTenant, affectedTenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForTenant, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId()); @@ -225,6 +226,9 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS if (!isNotificationConfigured(tenantId, NotificationType.RESOURCES_SHORTAGE)) { defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId()); } + if (!isNotificationConfigured(tenantId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST)) { + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId()); + } } else { var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE); var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes( diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 7cee3d04ae..7e82ae9472 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -52,12 +52,15 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.RateL import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -102,7 +105,17 @@ public class DefaultNotifications { .description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit") .build()) .build(); - + public static final DefaultNotification entitiesLimitIncreaseRequest = DefaultNotification.builder() + .name("Entities limit increase request for sysadmin") + .type(NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) + .subject("${entityType} limit increase request") + .text("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.") + .button("${increaseLimitActionLabel}").link("${increaseLimitLink}") + .emailTemplate(DefaultEmailTemplate.builder() + .subject("${entityType} limit increase request") + .body("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
${increaseLimitActionLabel}") + .build()) + .build(); public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder() .name("API feature warning notification for sysadmin") .type(NotificationType.API_USAGE_LIMIT) @@ -415,6 +428,8 @@ public class DefaultNotifications { private final String button; private final String link; + private final DefaultEmailTemplate emailTemplate; + private final DefaultRule rule; public NotificationTemplate toTemplate() { @@ -448,9 +463,17 @@ public class DefaultNotifications { } webTemplate.setAdditionalConfig(additionalConfig); webTemplate.setEnabled(true); - templateConfig.setDeliveryMethodsTemplates(Map.of( - NotificationDeliveryMethod.WEB, webTemplate - )); + + Map deliveryMethodsTemplates = new HashMap<>(); + deliveryMethodsTemplates.put(NotificationDeliveryMethod.WEB, webTemplate); + if (this.emailTemplate != null) { + EmailDeliveryMethodNotificationTemplate emailTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailTemplate.setSubject(this.emailTemplate.getSubject()); + emailTemplate.setBody(this.emailTemplate.getBody()); + emailTemplate.setEnabled(true); + deliveryMethodsTemplates.put(NotificationDeliveryMethod.EMAIL, emailTemplate); + } + templateConfig.setDeliveryMethodsTemplates(deliveryMethodsTemplates); template.setConfiguration(templateConfig); return template; } @@ -482,6 +505,13 @@ public class DefaultNotifications { } + @Data + @Builder(toBuilder = true) + public static class DefaultEmailTemplate { + private final String subject; + private final String body; + } + @Data @Builder(toBuilder = true) public static class DefaultRule { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index 76a7906b03..ffa88fe39c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -123,7 +123,8 @@ public abstract class DataValidator> { protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { - throw new EntitiesLimitException(tenantId, entityType); + long limit = apiLimitService.getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType)); + throw new EntitiesLimitException(tenantId, entityType, limit); } } diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index 93adc36c97..a07caed745 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -37,6 +37,7 @@ import { } from '@shared/models/notification.models'; import { User } from '@shared/models/user.model'; import { isNotEmptyStr } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; @Injectable({ providedIn: 'root' @@ -69,6 +70,10 @@ export class NotificationService { return this.http.post('/api/notification/request', notification, defaultHttpOptionsFromConfig(config)); } + public sendEntitiesLimitIncreaseRequest(entityType: EntityType, config?: RequestConfig): Observable { + return this.http.post(`/api/notification/entitiesLimitIncreaseRequest/${entityType}`, defaultHttpOptionsFromConfig(config)); + } + public getNotificationRequestById(id: string, config?: RequestConfig): Observable { return this.http.get(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index af204fc99b..f09e3321ff 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -109,6 +109,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } else if (errorCode !== Constants.serverErrorCode.credentialsExpired) { unhandled = true; } + } else if (errorCode && errorCode === Constants.serverErrorCode.entitiesLimitExceeded) { + if (!ignoreErrors) { + this.dialogService.entitiesLimitExceeded(errorResponse.error); + } } else if (errorResponse.status === 429) { if (resendRequest) { return this.retryRequest(req, next); diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index 7844ffbd2d..631c1224b3 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -36,6 +36,11 @@ import { ErrorAlertDialogData } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { + EntityLimitExceededDialogComponent, + EntityLimitExceededDialogData +} from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; @Injectable({ providedIn: 'root' @@ -125,6 +130,16 @@ export class DialogService { }).afterClosed(); } + entitiesLimitExceeded(entityLimitData: {entityType: EntityType, limit: number}): Observable { + return this.dialog.open(EntityLimitExceededDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: entityLimitData, + autoFocus: false + }).afterClosed(); + } + private permissionDenied() { this.alert( this.translate.instant('access.permission-denied'), diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts index 0f13409a3e..6fe3540854 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -181,6 +181,7 @@ export class TemplateNotificationDialogComponent private allowNotificationType(): NotificationType[] { const sysAdminAllowNotificationTypes = new Set([ NotificationType.ENTITIES_LIMIT, + NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, NotificationType.API_USAGE_LIMIT, NotificationType.NEW_PLATFORM_VERSION, NotificationType.RATE_LIMITS, diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html new file mode 100644 index 0000000000..3bc2591c24 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html @@ -0,0 +1,40 @@ + +
+ +
+
{{ 'entity.limit-reached' | translate }}
+
+
+
+
+ + +
+
+ {{ 'entity.request-sysadmin-text' | translate }} + {{ 'entity.login-here' | translate }} + {{ 'entity.to-increase-limit' | translate }} +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss new file mode 100644 index 0000000000..7d55e824b2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../scss/constants'; + +.entity-limit-exceeded-dialog { + max-width: 100%; + height: 100%; + + @media #{$mat-gt-xs} { + max-width: 500px; + max-height: 80vh; + } + + position: relative; + padding: 40px 40px 24px 40px; + display: flex; + flex-direction: column; + gap: 20px; + + .close-btn { + position: absolute; + top: 8px; + right: 8px; + color: rgba(0, 0, 0, 0.54); + } + + .entity-limit-exceeded-bg { + position: relative; + width: 100%; + height: 140px; + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $tb-primary-color; + mask-image: url(/assets/entity-limit-reached.svg); + -webkit-mask-image: url(/assets/entity-limit-reached.svg); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-size: contain; + mask-position: center; + -webkit-mask-position: center; + } + } + + .entity-limit-exceeded-title { + font-size: 24px; + font-style: normal; + font-weight: 500; + line-height: 32px; + color: rgba(0, 0, 0, 0.87); + text-align: center; + } + + .entity-limit-exceeded-container { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: center; + .entity-limit-exceeded-text { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + color: rgba(0, 0, 0, 0.76); + text-align: center; + } + } + + .request-entity-limit-increase-sysadmin-prompt { + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: rgba(0, 0, 0, 0.54); + } + +} diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts new file mode 100644 index 0000000000..7efabb0fcb --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Component, Inject, ViewEncapsulation } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthService } from '@core/auth/auth.service'; +import { TenantService } from '@core/http/tenant.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { NotificationService } from '@core/http/notification.service'; + +export interface EntityLimitExceededDialogData { + entityType: EntityType; + limit: number; +} + +// @dynamic +@Component({ + selector: 'tb-entity-limit-exceeded-dialog', + templateUrl: './entity-limit-exceeded-dialog.component.html', + styleUrls: ['./entity-limit-exceeded-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class EntityLimitExceededDialogComponent extends DialogComponent { + + limitReachedText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EntityLimitExceededDialogData, + public dialogRef: MatDialogRef, + private authService: AuthService, + private dialogs: DialogService, + private translate: TranslateService, + private tenantService: TenantService, + private notificationService: NotificationService) { + super(store, router, dialogRef); + + let entitiesText: string; + if (data.limit > 1) { + entitiesText = data.limit + ' ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).typePlural) as string).toLowerCase(); + } else { + entitiesText = '1 ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase(); + } + this.limitReachedText = this.translate.instant('entity.limit-reached-text', { entities: entitiesText, entity: (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase() }); + } + + cancel(): void { + this.dialogRef.close(); + } + + requestLimitIncrease($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.notificationService.sendEntitiesLimitIncreaseRequest(this.data.entityType).subscribe( + () => { + this.dialogRef.close(); + this.dialogs.alert( + this.translate.instant('entity.increase-limit-request-sent-title'), + this.translate.instant('entity.increase-limit-request-sent-text'), + this.translate.instant('action.close') + ); + } + ); + } + + loginAsSysAdmin($event: Event) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + this.tenantService.getTenant(getCurrentAuthUser(this.store).tenantId).subscribe( + (tenant) => { + this.authService.redirectUrl = `/tenantProfiles/${tenant.tenantProfileId.id}`; + this.authService.logout(); + } + ); + } + +} diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts index 8f7d9365f2..77be7ff7cd 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.ts +++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts @@ -157,6 +157,8 @@ export class NotificationComponent implements OnInit { return {color: AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)}; } else if (this.notification.type === NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) { return {color: '#D12730'}; + } else if (this.notification.type === NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) { + return {color: '#305680'}; } return null; } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 68391a42f3..72315e8ed5 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -31,6 +31,7 @@ export const Constants = { itemNotFound: 32, tooManyRequests: 33, tooManyUpdates: 34, + entitiesLimitExceeded: 41, passwordViolation: 45 }, entryPoints: { diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 63d7edd2a2..f9abf6462b 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -526,6 +526,7 @@ export enum NotificationType { ALARM_ASSIGNMENT = 'ALARM_ASSIGNMENT', RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT = 'RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT', ENTITIES_LIMIT = 'ENTITIES_LIMIT', + ENTITIES_LIMIT_INCREASE_REQUEST = 'ENTITIES_LIMIT_INCREASE_REQUEST', API_USAGE_LIMIT = 'API_USAGE_LIMIT', NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION', RULE_NODE = 'RULE_NODE', @@ -544,6 +545,7 @@ export const NotificationTypeIcons = new Map([ [NotificationType.ALARM_ASSIGNMENT, 'assignment_turned_in'], [NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'], [NotificationType.ENTITIES_LIMIT, 'data_thresholding'], + [NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, 'mdi:file-cog'], [NotificationType.API_USAGE_LIMIT, 'insert_chart'], [NotificationType.TASK_PROCESSING_FAILURE, 'warning'], [NotificationType.RESOURCES_SHORTAGE, 'warning'] @@ -623,6 +625,12 @@ export const NotificationTemplateTypeTranslateMap = new Map + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md b/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md new file mode 100644 index 0000000000..388f0f532e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md @@ -0,0 +1,46 @@ +#### Entity count limit increase request notification templatization + +
+
+ +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `entityType` - one of: 'Device', 'Asset', 'Customer', 'User', 'Dashboard', 'Rule chain', 'Edge'; +* `userEmail` - email of the user who sends the request; +* `increaseLimitActionLabel` - label of the button used to open Limits Management page, for ex: 'Set new limit'; +* `increaseLimitLink` - link to the Limits Management page; +* `baseUrl` - used to construct the full URL for the Limits Management page in email notifications; + +Parameter names must be wrapped using `${...}`. For example: `${userEmail}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${userEmail:upperCase}` +* `lowerCase`, for example - `${userEmail:lowerCase}` +* `capitalize`, for example - `${userEmail:capitalize}` + +
+ +##### Examples + +Let's assume the notification about the increasing limit of the maximum allowed devices for the tenant. +The following template: + +```text +${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit. +{:copy-code} +``` + +will be transformed to: + +```text +johndoe@company.com has reached the maximum number of devices allowed and is requesting an increase to the device limit. +``` + +
+ +
+
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 477c1fa012..da71326f10 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3174,7 +3174,15 @@ "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }", "type-mobile-app-bundle": "Mobile bundle", "type-mobile-app-bundles": "Mobile bundles", - "list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }" + "list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }", + "limit-reached": "Limit reached", + "limit-reached-text": "You have reached the limit of {{ entities }}. To add more, please ask your System Administrator to increase your {{ entity }} limit.", + "request-limit-increase": "Request limit increase", + "request-sysadmin-text": "Are you the System Administrator?", + "login-here": "Login here", + "to-increase-limit": "to increase limit.", + "increase-limit-request-sent-title": "We have sent an automated request to your System Administrator to increase the limit", + "increase-limit-request-sent-text": "Please allow some time for them to review the request and update the settings. You may need to refresh this page to see the changes." }, "entity-field": { "created-time": "Created time", @@ -4700,6 +4708,7 @@ "api-usage-limit": "API usage limit", "device-activity": "Device activity", "entities-limit": "Entities limit", + "entities-limit-increase-request": "Entities limit increase request", "entity-action": "Entity action", "general": "General", "rule-engine-lifecycle-event": "Rule engine lifecycle event", From 09eb511599225583100fe2eb2ab7c1f79bc98f48 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 16:51:40 +0200 Subject: [PATCH 036/147] Fix key_dictionary race causing cached keyId 0 --- .../dictionary/KeyDictionaryCompositeKey.java | 2 +- .../sqlts/dictionary/KeyDictionaryEntry.java | 5 +- .../sqlts/dictionary/JpaKeyDictionaryDao.java | 59 ++++------ .../dictionary/KeyDictionaryRepository.java | 4 + .../dictionary/KeyDictionaryDaoTest.java | 111 ++++++++++++++++++ 5 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java index 4f3285b9bf..00e49ea703 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java @@ -25,7 +25,7 @@ import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor -public class KeyDictionaryCompositeKey implements Serializable{ +public class KeyDictionaryCompositeKey implements Serializable { @Transient private static final long serialVersionUID = -4089175869616037523L; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java index a95c7a2bc6..d98105c0bc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java @@ -36,8 +36,7 @@ public final class KeyDictionaryEntry { @Column(name = KEY_COLUMN) private String key; - @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int") - @Generated - private int keyId; + @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false) + private Integer keyId; } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index c14c069f23..1ef2c2d6aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts.dictionary; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -48,43 +46,32 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService @Transactional(propagation = Propagation.NOT_SUPPORTED) @Override public Integer getOrSaveKeyId(String strKey) { - Integer keyId = keyDictionaryMap.get(strKey); - if (keyId == null) { - Optional tsKvDictionaryOptional; - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - if (tsKvDictionaryOptional.isEmpty()) { - creationLock.lock(); - try { - keyId = keyDictionaryMap.get(strKey); - if (keyId != null) { - return keyId; - } - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - if (tsKvDictionaryOptional.isEmpty()) { - KeyDictionaryEntry keyDictionaryEntry = new KeyDictionaryEntry(); - keyDictionaryEntry.setKey(strKey); - try { - KeyDictionaryEntry saved = keyDictionaryRepository.save(keyDictionaryEntry); - keyDictionaryMap.put(saved.getKey(), saved.getKeyId()); - keyId = saved.getKeyId(); - } catch (DataIntegrityViolationException | ConstraintViolationException e) { - tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); - KeyDictionaryEntry dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get KeyDictionaryEntry entity from DB!")); - keyDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId()); - keyId = dictionary.getKeyId(); - } - } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - } - } finally { - creationLock.unlock(); + Integer cached = keyDictionaryMap.get(strKey); + if (cached != null) { + return cached; + } + creationLock.lock(); + try { + Integer keyId = keyDictionaryMap.get(strKey); + if (keyId != null) { + return keyId; + } + keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); + if (keyId == null || keyId == 0) { + log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); + KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(strKey); + Optional entryOpt = keyDictionaryRepository.findById(id); + if (entryOpt.isEmpty() || + entryOpt.get().getKeyId() == null || + entryOpt.get().getKeyId() == 0) { + throw new IllegalStateException("Failed to resolve keyId for string key: " + strKey + " after fallback. keyId: " + keyId); } - } else { - keyId = tsKvDictionaryOptional.get().getKeyId(); - keyDictionaryMap.put(strKey, keyId); } + keyDictionaryMap.put(strKey, keyId); + return keyId; + } finally { + creationLock.unlock(); } - return keyId; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java index d264cd9966..a1141b62ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -31,4 +32,7 @@ public interface KeyDictionaryRepository extends JpaRepository findAll(Pageable pageable); + @Query(value = "INSERT INTO key_dictionary (key) VALUES (:key) ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key RETURNING key_id", nativeQuery = true) + Integer upsertAndGetKeyId(@Param("key") String key); + } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java new file mode 100644 index 0000000000..11c14c99f4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sqlts.dictionary; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; +import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; +import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@DaoSqlTest +public class KeyDictionaryDaoTest extends AbstractServiceTest { + + @Autowired + private KeyDictionaryDao keyDictionaryDao; + + @Autowired + private KeyDictionaryRepository keyDictionaryRepository; + + private static final String KEY = "testKeyDictionaryDaoTestKey"; + + @Test + public void testGetOrSaveKeyId_concurrent() throws Exception { + int threads = 8; + ExecutorService executor = Executors.newFixedThreadPool(threads); + + CountDownLatch allReady = new CountDownLatch(threads); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch allDone = new CountDownLatch(threads); + + Integer[] keyIds = new Integer[threads]; + + try { + for (int i = 0; i < threads; i++) { + final int idx = i; + executor.submit(() -> { + allReady.countDown(); + try { + // wait until all threads are ready + start.await(); + // concurrent call + Integer id = keyDictionaryDao.getOrSaveKeyId(KEY); + keyIds[idx] = id; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + allDone.countDown(); + } + }); + } + + // ensure all threads are queued + allReady.await(5, TimeUnit.SECONDS); + // fire the start gun + start.countDown(); + // wait for all to finish + allDone.await(10, TimeUnit.SECONDS); + } finally { + executor.shutdownNow(); + } + + // basic sanity + for (int i = 0; i < threads; i++) { + assertThat(keyIds[i]) + .as("keyId[%s]", i) + .isNotNull() + .isGreaterThan(0); + } + + // all threads must see the same keyId + int first = keyIds[0]; + assertThat(first).isGreaterThan(0); + assertThat(Arrays.stream(keyIds).distinct().count()) + .as("all threads should get the same keyId") + .isEqualTo(1); + + // DB must have exactly one row for this key and the same id + KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(KEY); + Optional entry = keyDictionaryRepository.findById(id); + + assertThat(entry.isPresent()).isTrue(); + assertThat(entry.get().getKeyId()).isEqualTo(first); + + keyDictionaryRepository.deleteById(id); + } + +} From d66e9ecf74b430ac6ab9bee4fa6b863157fbb419 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 17:45:00 +0200 Subject: [PATCH 037/147] Added new lines to the end of files --- .../server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java | 2 +- .../server/dao/sqlts/dictionary/KeyDictionaryRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java index d98105c0bc..8365e8facc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java @@ -39,4 +39,4 @@ public final class KeyDictionaryEntry { @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false) private Integer keyId; -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java index a1141b62ff..e836cedb19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java @@ -35,4 +35,4 @@ public interface KeyDictionaryRepository extends JpaRepository Date: Mon, 8 Dec 2025 17:48:02 +0200 Subject: [PATCH 038/147] Fix NPE --- .../java/org/thingsboard/server/dao/sql/user/JpaUserDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 0cb5be92ee..6b77460bfb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -141,7 +141,7 @@ public class JpaUserDao extends JpaAbstractDao implements User @Override public UserAuthDetails findUserAuthDetailsByUserId(UUID tenantId, UUID userId) { TbPair result = userRepository.findUserAuthDetailsByUserId(userId); - return new UserAuthDetails(result.getFirst().toData(), result.getSecond()); + return result != null ? new UserAuthDetails(result.getFirst().toData(), result.getSecond()) : null; } @Override From c3cdd11f579f69ba12d1dc5c50c4d16186ec717a Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 8 Dec 2025 18:13:30 +0200 Subject: [PATCH 039/147] fixes --- .../alarm-rules/alarm-rules-table-config.ts | 12 ++---------- .../calculated-fields-table-config.ts | 8 ++++---- .../home/dialogs/events-dialog.component.html | 2 +- .../modules/home/dialogs/events-dialog.component.ts | 8 ++++---- .../home/pages/rulechain/rulechain-page.component.ts | 2 +- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 15c7cdaeb1..0cb6069ee9 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -264,25 +264,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private openDebugEventsDialog(calculatedField: CalculatedField): void { - const customCellActionEnabledFn = (event: DebugEvent) => { - return (calculatedField.type === CalculatedFieldType.SCRIPT || - (calculatedField.type === CalculatedFieldType.PROPAGATION && - calculatedField.configuration.applyExpressionToResolvedArguments) - ) && !!(event as DebugEvent).body.arguments; - }; - this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { title: 'alarm-rule.debugging', tenantId: this.tenantId, - value: calculatedField.id, + entityId: calculatedField.id, debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, - onDebugEventSelected: null, - customCellActionEnabledFn: customCellActionEnabledFn + debugActionEnabledFn: () => false } }) .afterClosed() diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 18dcb28a2e..60c2878802 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -233,7 +233,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + const debugActionEnabledFn = (event: DebugEvent) => { return (calculatedField.type === CalculatedFieldType.SCRIPT || (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments) @@ -251,12 +251,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig diff --git a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts index 613f4e9c15..d5339de3b2 100644 --- a/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts @@ -28,13 +28,13 @@ import { BaseData, HasId } from '@shared/models/base-data'; export interface EventsDialogData { title: string; tenantId: string; - value: EntityId; + entityId: EntityId; debugEventTypes: Array; defaultEventType: DebugEventType; disabledEventTypes?: Array; functionTestButtonLabel?: string; onDebugEventSelected?: (event: any, dialogRef: MatDialogRef) => void; - customCellActionEnabledFn?: (event: BaseData) => boolean; + debugActionEnabledFn?: (event: BaseData) => boolean; } @Component({ @@ -54,8 +54,8 @@ export class EventsDialogComponent extends DialogComponent 0) { - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.customCellActionEnabledFn; + if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) { + this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn; } this.eventsTable.entitiesTable.updateData(); } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index b1cf2eddf2..7f751a0bfe 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1682,7 +1682,7 @@ export class RuleChainPageComponent extends PageComponent debugEventTypes: [DebugEventType.DEBUG_RULE_NODE], defaultEventType: DebugEventType.DEBUG_RULE_NODE, tenantId: this.ruleChain.tenantId.id, - value: this.editingRuleNode.ruleNodeId, + entityId: this.editingRuleNode.ruleNodeId, functionTestButtonLabel: this.ruleNodeTestButtonLabel, onDebugEventSelected: this.onDebugEventSelected.bind(this) } From 3243c2a2f2555d5a2a0a68dffce36fd6c239a611 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 18:36:01 +0200 Subject: [PATCH 040/147] Added find before lock for the startup of application --- .../dao/sqlts/dictionary/JpaKeyDictionaryDao.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 1ef2c2d6aa..bcb9284371 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -50,6 +50,15 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService if (cached != null) { return cached; } + var compositeKey = new KeyDictionaryCompositeKey(strKey); + Optional entryOpt = keyDictionaryRepository.findById(compositeKey); + if (entryOpt.isPresent()) { + Integer keyId = entryOpt.get().getKeyId(); + if (keyId != null) { + keyDictionaryMap.put(strKey, keyId); + return keyId; + } + } creationLock.lock(); try { Integer keyId = keyDictionaryMap.get(strKey); @@ -59,8 +68,7 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); if (keyId == null || keyId == 0) { log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); - KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(strKey); - Optional entryOpt = keyDictionaryRepository.findById(id); + entryOpt = keyDictionaryRepository.findById(compositeKey); if (entryOpt.isEmpty() || entryOpt.get().getKeyId() == null || entryOpt.get().getKeyId() == 0) { From f379c354c56921720c1189d01fd1aae125711fe4 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 8 Dec 2025 19:10:42 +0200 Subject: [PATCH 041/147] rollback logback changes for black-box tests --- .../src/test/resources/tb-node/conf/logback.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml index f9fd25d44a..aa88f0bbb0 100644 --- a/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml +++ b/msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml @@ -48,11 +48,7 @@ - - - - From a9a179fe48dc777aeec9e875622178c3d35f4bff Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 8 Dec 2025 20:29:06 +0200 Subject: [PATCH 042/147] Added AiModel Fetcher and support of Delete AiModel on Edge --- .../service/edge/rpc/EdgeSyncCursor.java | 2 + .../rpc/fetch/AiModelEdgeEventFetcher.java | 48 +++++++++++++++++ .../processor/ai/AiModelEdgeProcessor.java | 53 +++++++------------ .../processor/ai/BaseAiModelProcessor.java | 10 ++++ 4 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index adab9b812f..20de7186b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; @@ -92,6 +93,7 @@ public class EdgeSyncCursor { fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); + fetchers.add(new AiModelEdgeEventFetcher(ctx.getAiModelService())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java new file mode 100644 index 0000000000..8cba05402b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.ai.AiModelService; + +@AllArgsConstructor +@Slf4j +public class AiModelEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final AiModelService aiModelService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return aiModelService.findAiModelsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AiModel aiModel) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, + EdgeEventActionType.ADDED, aiModel.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java index 74ca7ac27a..cf1c9d9abd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.edge.Edge; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -53,15 +51,17 @@ public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiMode try { edgeSynchronizationManager.getEdgeId().set(edge.getId()); - switch (aiModelUpdateMsg.getMsgType()) { - case ENTITY_CREATED_RPC_MESSAGE: - case ENTITY_UPDATED_RPC_MESSAGE: + return switch (aiModelUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge); - return Futures.immediateFuture(null); - case UNRECOGNIZED: - default: - return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType()); - } + yield Futures.immediateFuture(null); + } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteAiModel(tenantId, edge, aiModelId); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType()); + }; } catch (DataValidationException e) { return Futures.immediateFailedFuture(e); } finally { @@ -95,36 +95,21 @@ public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiMode return null; } - @Override - public EdgeEventType getEdgeEventType() { - return EdgeEventType.AI_MODEL; - } - private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) { Pair resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg); - Boolean wasCreated = resultPair.getFirst(); - if (wasCreated) { - pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId); + Boolean created = resultPair.getFirst(); + if (created) { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + aiModel.ifPresent(model -> pushEntityEventToRuleEngine(tenantId, edge, model, TbMsgType.ENTITY_CREATED)); } - Boolean nameWasUpdated = resultPair.getSecond(); - if (nameWasUpdated) { + Boolean aiModelNameUpdated = resultPair.getSecond(); + if (aiModelNameUpdated) { saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null); } } - private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) { - try { - Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); - if (aiModel.isPresent()) { - String aiModelAsString = JacksonUtil.toString(aiModel.get()); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId()); - pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData); - } else { - log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId); - } - } catch (Exception e) { - log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e); - } + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.AI_MODEL; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java index cb1d27e0a1..c6d50abe28 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java @@ -22,8 +22,10 @@ import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -78,4 +80,12 @@ public abstract class BaseAiModelProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } + protected void deleteAiModel(TenantId tenantId, Edge edge, AiModelId aiModelId) { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + if (aiModel.isPresent()) { + edgeCtx.getAiModelService().deleteByTenantIdAndId(tenantId, aiModelId); + pushEntityEventToRuleEngine(tenantId, edge, aiModel.get(), TbMsgType.ENTITY_DELETED); + } + } + } From 6253cc699d13475b6e3e210d2b224835b8d6202f Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Dec 2025 08:52:44 +0200 Subject: [PATCH 043/147] fixed tests --- .../cf/ctx/state/GeofencingCalculatedFieldStateTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index b3ce7bbb44..e717392979 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -247,7 +247,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "ENTERED") .put("allowedZonesStatus", "INSIDE") @@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED") ); @@ -406,7 +406,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( + assertThat(result.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesStatus", "INSIDE") .put("restrictedZonesStatus", "OUTSIDE") From c532cbaa5e0fe0bb94e07016a144dfdfc3e722e4 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Dec 2025 10:04:41 +0200 Subject: [PATCH 044/147] added tests --- .../cf/ctx/state/CalculatedFieldCtx.java | 21 +- .../common/data/cf/configuration/Output.java | 21 ++ .../data/cf/configuration/OutputStrategy.java | 2 - .../CalculatedFieldOutputTest.java | 202 ++++++++++++++++++ 4 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 7e5d666ca5..7f515a5c9a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -657,7 +657,7 @@ public class CalculatedFieldCtx implements Closeable { if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { return true; } - if (hasOutputChanges(other.output)) { + if (output.hasContextOnlyChanges(other.output)) { return true; } if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig @@ -727,25 +727,6 @@ public class CalculatedFieldCtx implements Closeable { return false; } - private boolean hasOutputChanges(Output otherOutput) { - if (!output.getType().equals(otherOutput.getType())) { - return true; - } - if (!Objects.equals(output.getName(), otherOutput.getName())) { - return true; - } - if (output.getScope() != (otherOutput.getScope())) { - return true; - } - if (!Objects.equals(output.getDecimalsByDefault(), otherOutput.getDecimalsByDefault())) { - return true; - } - if (output.getStrategy().hasContextOnlyChanges(otherOutput.getStrategy())) { - return true; - } - return false; - } - private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 848ad759e8..fe85ff03e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.AttributeScope; +import java.util.Objects; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -50,4 +52,23 @@ public interface Output { void setDecimalsByDefault(Integer decimalsByDefault); + default boolean hasContextOnlyChanges(Output other) { + if (!getType().equals(other.getType())) { + return true; + } + if (!Objects.equals(getName(), other.getName())) { + return true; + } + if (getScope() != (other.getScope())) { + return true; + } + if (!Objects.equals(getDecimalsByDefault(), other.getDecimalsByDefault())) { + return true; + } + if (getStrategy().hasContextOnlyChanges(other.getStrategy())) { + return true; + } + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index 252af82ef7..66b156ca8a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -22,10 +22,8 @@ public interface OutputStrategy { @JsonIgnore OutputStrategyType getType(); - @JsonIgnore boolean hasContextOnlyChanges(OutputStrategy other); - @JsonIgnore boolean hasRefreshContextOnlyChanges(OutputStrategy other); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java new file mode 100644 index 0000000000..52afa47fbd --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldOutputTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class CalculatedFieldOutputTest { + + @Test + public void testHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenNameChanged_shouldReturnTrue() { + TimeSeriesOutput output = new TimeSeriesOutput(); + TimeSeriesOutput newOutput = new TimeSeriesOutput(); + newOutput.setName("new"); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenScopeChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + output.setScope(AttributeScope.SHARED_SCOPE); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setScope(AttributeScope.SERVER_SCOPE); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenDecimalsByDefaultChanged_shouldReturnTrue() { + AttributesOutput output = new AttributesOutput(); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setDecimalsByDefault(2); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyHasContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(true); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isTrue(); + } + + @Test + public void testHasContextOnlyChanges_whenStrategyDoesNotHaveContextOnlyChanges_shouldReturnTrue() { + AttributesOutputStrategy outputStrategy = mock(AttributesRuleChainOutputStrategy.class); + given(outputStrategy.hasContextOnlyChanges(any())).willReturn(false); + + AttributesOutput output = new AttributesOutput(); + output.setStrategy(outputStrategy); + AttributesOutput newOutput = new AttributesOutput(); + + assertThat(output.hasContextOnlyChanges(newOutput)).isFalse(); + } + + /* .hasContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesRuleChainOutputStrategy newStrategy = new AttributesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSaveAttributesChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, true, true, false, false); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesRuleChainOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + AttributesRuleChainOutputStrategy strategy = new AttributesRuleChainOutputStrategy(); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, false, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenTypeChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + TimeSeriesRuleChainOutputStrategy newStrategy = new TimeSeriesRuleChainOutputStrategy(); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSaveLatestChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, false, false, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, false, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenSendWsUpdateChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, false, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesRuleChainOutputStrategyHasContextOnlyChanges_whenProcessCfsChanged_shouldReturnTrue() { + TimeSeriesRuleChainOutputStrategy strategy = new TimeSeriesRuleChainOutputStrategy(); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, false); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isTrue(); + } + + /* .hasRefreshContextOnlyChanges() tests*/ + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenUpdateAttrOnValueChangedChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(true, false, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testAttributesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenSendAttrUpdatedNotificationChanged_shouldReturnTrue() { + AttributesImmediateOutputStrategy strategy = new AttributesImmediateOutputStrategy(false, true, true, false, true); + AttributesImmediateOutputStrategy newStrategy = new AttributesImmediateOutputStrategy(true, true, true, false, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + + @Test + public void testTimeSeriesImmediateOutputStrategyHasRefreshContextOnlyChanges_whenTtlChanged_shouldReturnTrue() { + TimeSeriesImmediateOutputStrategy strategy = new TimeSeriesImmediateOutputStrategy(0, true, true, true, true); + TimeSeriesImmediateOutputStrategy newStrategy = new TimeSeriesImmediateOutputStrategy(300, true, true, true, true); + + assertThat(strategy.hasContextOnlyChanges(newStrategy)).isFalse(); + assertThat(strategy.hasRefreshContextOnlyChanges(newStrategy)).isTrue(); + } + +} From bde19743622f5fa34f0eb77049c36262587e2a87 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 9 Dec 2025 10:21:16 +0200 Subject: [PATCH 045/147] UI: Fixed calculated field bugs and redesign alarm rule --- .../alarm-rule-dialog.component.html | 10 +- .../alarm-rule-dialog.component.scss | 4 + .../alarm-rule-dialog.component.ts | 40 +--- ...alarm-rule-condition-dialog.component.html | 205 +++++++++--------- .../cf-alarm-rule-condition.component.html | 2 +- .../cf-alarm-schedule-dialog.component.html | 23 +- .../create-cf-alarm-rules.component.html | 2 +- .../alarm-rule-filter-list.component.scss | 1 + ...-rule-filter-predicate-list.component.scss | 1 + .../alarm-rule-filter-predicate.component.ts | 11 +- ...lculated-field-argument-panel.component.ts | 1 + ...lated-field-arguments-table.component.html | 2 +- ...lated-field-arguments-table.component.scss | 4 + ...-geofencing-zone-groups-panel.component.ts | 2 +- ...eofencing-zone-groups-table.component.html | 2 +- ...culated-field-metrics-table.component.html | 2 +- .../assets/locale/locale.constant-en_US.json | 8 +- 17 files changed, 166 insertions(+), 154 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 343c3123ab..7e706bfca9 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -84,7 +84,7 @@
-
{{ 'calculated-fields.arguments' | translate }}
+
{{ 'calculated-fields.arguments' | translate }}
-
{{ 'alarm-rule.create-conditions' | translate }}
+
{{ 'alarm-rule.create-conditions' | translate }}
-
{{ 'alarm-rule.clear-condition' | translate }}
+
{{ 'alarm-rule.clear-condition' | translate }}
@@ -118,7 +118,7 @@
- alarm-rule.no-clear-alarm-rule + alarm-rule.no-clear-alarm-rule
- -
- -
+ } @else { +
+
+ {{ 'alarm-rule.script' | translate }} +
+ +
{{ 'alarm-rule.tbel' | translate }} +
+ +
+
+ +
+
+ }
- } -
- @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { -
- - - {{ 'alarm-rule.filter-preview' | translate }} - - -
- @if (specText) { - {{ specText }} - } - @if (conditionFormGroup.get('expression.filters').value?.length) { - - - } @else { - {{ 'alarm-rule.no-filter-preview' | translate }} - } -
-
-
-
- } + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
+
-
-
{{ 'alarm-rule.condition-settings' | translate }}
- - alarm-rule.condition-type - - +
+
+
{{ 'alarm-rule.type' | translate }}
+ + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - - - @if (isNoData) { - alarm-rule.condition-type-hint - } - + + +
+ @if (isNoData) { +
+ {{ 'alarm-rule.condition-type-hint' | translate }} +
+ } @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index 4892fcdecd..22fc01f9de 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -31,7 +31,7 @@ [nowrap]="true" [specText]="specText" required - addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}"> + addFilterPrompt="{{ 'alarm-rule.enter-alarm-rule-condition-prompt' | translate }}"> diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html index 2d12355f48..4caecbcc34 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -19,10 +19,6 @@

{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}

- - {{ 'alarm-rule.static-schedule' | translate }} - {{ 'alarm-rule.dynamic-schedule' | translate }} -
- - +
+
+
+
{{ 'alarm-rule.mode' | translate }}
+ + {{ 'alarm-rule.static-schedule' | translate }} + {{ 'alarm-rule.dynamic-schedule' | translate }} + +
+ + +
+
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.no-arguments' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss index 77ced9edef..f4f42f1b54 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss @@ -41,6 +41,10 @@ } } + .no-data-found { + font-size: 16px; + } + .max-args-warning { .mat-icon { color: #FAA405; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 0bbb72efb5..3484e717cb 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -278,7 +278,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); this.geofencingFormGroup.get('perimeterKeyName').reset(''); - const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type); + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner, ArgumentEntityType.RelationQuery].includes(type); this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable'](); if (!isEntityWithId) { this.entityNameSubject.next(null); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index 67ec7f097b..20db5c732d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -128,7 +128,7 @@ }
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.no-zone-configured' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index 209971f6e7..ff969775d5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -102,7 +102,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base required"> {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
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 ad6a779e02..06296ed045 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1403,7 +1403,7 @@ "target-entity": "Target entity", "target-entities": "Target entities", "alarm-type": "Alarm type", - "alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", + "alarm-type-hint": "Unique identifier (e.g., HighTempAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", "alarm-type-required": "Alarm type is required.", "alarm-type-pattern": "Alarm type is invalid.", "alarm-type-max-length": "Alarm type should be less than 256 characters.", @@ -1513,8 +1513,8 @@ "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", - "create-conditions": "Alarm Trigger Conditions", - "clear-condition": "Alarm Clear Condition", + "create-conditions": "Trigger conditions", + "clear-condition": "Clear condition", "no-clear-alarm-rule": "No clear condition configured.", "advanced-settings": "Advanced settings", "propagate-alarm": "Propagate alarm to related entities", @@ -1532,6 +1532,8 @@ "alarm-rule-entity-list": "Entity list", "missing-for": "missing for", "time-unit": "Unit", + "mode": "Mode", + "type": "Type", "value-required": "Value is required.", "min-value": "Value must be 0 or greater.", "argument-in-use": "Argument is used as general argument.", From 5d9a3351936dcce8c9c044f44ab41e08f8490e08 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 9 Dec 2025 10:51:16 +0200 Subject: [PATCH 046/147] Added AI model delete test to cloud --- .../server/edge/AiModelEdgeTest.java | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java index 30c8448f5b..8daed92a9a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java @@ -32,8 +32,11 @@ import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; @DaoSqlTest public class AiModelEdgeTest extends AbstractEdgeTest { @@ -42,7 +45,7 @@ public class AiModelEdgeTest extends AbstractEdgeTest { private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel"; @Test - public void testAiModel_create_update_delete() throws Exception { + public void testAiModel_create_update_delete_fromCloud() throws Exception { // create AiModel AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); @@ -91,26 +94,29 @@ public class AiModelEdgeTest extends AbstractEdgeTest { } @Test - public void testSendAiModelToCloud() throws Exception { - AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); - UUID uuid = Uuids.timeBased(); - UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); - - checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName()); - } - - @Test - public void testUpdateAiModelNameOnCloud() throws Exception { + public void testAiModel_create_update_delete_toCloud() throws Exception { + // create AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); UUID uuid = Uuids.timeBased(); UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName()); + // update aiModel.setName(UPDATED_AI_MODEL_NAME); UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName()); + + // delete + UplinkMsg deleteUplinkMsg = getDeleteUplinkMsg(uuid); + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(deleteUplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> + doGet("/api/ai/model/" + uuid, AiModel.class, status().isNotFound()) + ); } @Test @@ -164,6 +170,20 @@ public class AiModelEdgeTest extends AbstractEdgeTest { return aiModel; } + private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException { + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + AiModelUpdateMsg.Builder aiModelDeleteMsgBuilder = AiModelUpdateMsg.newBuilder(); + aiModelDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE); + aiModelDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + aiModelDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(aiModelDeleteMsgBuilder); + + upLinkMsgBuilder.addAiModelUpdateMsg(aiModelDeleteMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + return upLinkMsgBuilder.build(); + } + private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder(); From c0af057590273148da4e65fdb147ba3a9c38a23b Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 9 Dec 2025 11:31:02 +0200 Subject: [PATCH 047/147] refactoring logic in getOrSaveKeyId method --- .../sqlts/dictionary/JpaKeyDictionaryDao.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index bcb9284371..9d23bdbcb5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -51,32 +51,29 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return cached; } var compositeKey = new KeyDictionaryCompositeKey(strKey); - Optional entryOpt = keyDictionaryRepository.findById(compositeKey); - if (entryOpt.isPresent()) { - Integer keyId = entryOpt.get().getKeyId(); - if (keyId != null) { - keyDictionaryMap.put(strKey, keyId); - return keyId; - } + Optional existingId = keyDictionaryRepository.findById(compositeKey) + .map(KeyDictionaryEntry::getKeyId) + .filter(id -> id != 0); + if (existingId.isPresent()) { + return cacheAndReturn(strKey, existingId.get()); } creationLock.lock(); try { - Integer keyId = keyDictionaryMap.get(strKey); - if (keyId != null) { - return keyId; + Integer fromCache = keyDictionaryMap.get(strKey); + if (fromCache != null) { + return fromCache; } - keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); - if (keyId == null || keyId == 0) { - log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); - entryOpt = keyDictionaryRepository.findById(compositeKey); - if (entryOpt.isEmpty() || - entryOpt.get().getKeyId() == null || - entryOpt.get().getKeyId() == 0) { - throw new IllegalStateException("Failed to resolve keyId for string key: " + strKey + " after fallback. keyId: " + keyId); - } + Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); + if (keyId != null && keyId != 0) { + return cacheAndReturn(strKey, keyId); } - keyDictionaryMap.put(strKey, keyId); - return keyId; + log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); + keyId = keyDictionaryRepository.findById(compositeKey) + .map(KeyDictionaryEntry::getKeyId) + .filter(id -> id != 0) + .orElseThrow(() -> new IllegalStateException( + "Failed to resolve keyId for string key: " + strKey + " after fallback.")); + return cacheAndReturn(strKey, keyId); } finally { creationLock.unlock(); } @@ -93,4 +90,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink))); } + private Integer cacheAndReturn(String key, Integer keyId) { + keyDictionaryMap.put(key, keyId); + return keyId; + } + } From ce3b2579421e6dad5418f19c087e4e036ef7c03b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 12:05:11 +0200 Subject: [PATCH 048/147] clean up and propagation of limits values --- .../widget/lib/chart/time-series-chart.ts | 27 ++++---- .../common/axis-scale-row.component.scss | 15 ----- .../common/axis-scale-row.component.ts | 67 +++++-------------- ...me-series-chart-axis-settings.component.ts | 16 +---- ...ime-series-chart-y-axes-panel.component.ts | 31 +++++---- ...ime-series-chart-y-axis-row.component.html | 10 +-- .../time-series-chart-y-axis-row.component.ts | 24 ++++++- 7 files changed, 75 insertions(+), 115 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index d429438f43..8568e4b13b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -594,10 +594,10 @@ export class TbTimeSeriesChart { } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); if (isDefinedAndNotNull(axisSettings.min)) { - this.processYAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.min as ValueSourceConfig, 'min', yAxis, axisLimitDatasources, unitConvertor); } if (isDefinedAndNotNull(axisSettings.max)) { - this.processYAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor); + this.processYAxisLimit(axisSettings.max as ValueSourceConfig, 'max', yAxis, axisLimitDatasources, unitConvertor); } this.yAxisList.push(yAxis); } @@ -605,22 +605,21 @@ export class TbTimeSeriesChart { } private processYAxisLimit( - limit: any, + limit: ValueSourceConfig, limitType: 'min' | 'max', yAxis: TimeSeriesChartYAxis, axisLimitDatasources: Datasource[], unitConvertor?: (value: number) => number ): void { if (limit && typeof limit === 'object' && 'type' in limit) { - const axisLimit = limit as ValueSourceConfig; - if (axisLimit.type === ValueSourceType.latestKey) { + if (limit.type === ValueSourceType.latestKey) { let latestDataKey: DataKey = null; if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === axisLimit.latestKey) || - (d.type !== DataKeyType.function && d.name === axisLimit.latestKey && - d.type === axisLimit.latestKeyType)); + (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType)); if (latestDataKey) { break; } @@ -633,8 +632,8 @@ export class TbTimeSeriesChart { yAxis.maxLatestDataKey = latestDataKey; } } - } else if (axisLimit.type === ValueSourceType.entity) { - const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias); + } else if (limit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(limit.entityAlias); if (entityAliasId) { let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); const entityDataKey: DataKey = { @@ -651,16 +650,16 @@ export class TbTimeSeriesChart { } else { datasource = { type: DatasourceType.entity, - name: axisLimit.entityAlias, - aliasName: axisLimit.entityAlias, + name: limit.entityAlias, + aliasName: limit.entityAlias, entityAliasId, dataKeys: [entityDataKey] }; axisLimitDatasources.push(datasource); } } - } else if (axisLimit.type === ValueSourceType.constant) { - const value = unitConvertor ? unitConvertor(axisLimit.value) : axisLimit.value; + } else if (limit.type === ValueSourceType.constant) { + const value = unitConvertor ? unitConvertor(limit.value) : limit.value; if (limitType === 'min') { yAxis.option.min = value; } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss deleted file mode 100644 index 1a4214be18..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 9962bdeed9..f01336d22e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -38,7 +38,6 @@ import { merge } from 'rxjs'; @Component({ selector: 'tb-axis-scale-row', templateUrl: './axis-scale-row.component.html', - styleUrl: './axis-scale-row.component.scss', providers: [ { provide: NG_VALUE_ACCESSOR, @@ -101,7 +100,6 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali }); this.latestKeyFormControl = this.fb.control(null, [Validators.required]); this.entityKeyFormControl = this.fb.control(null, [Validators.required]); - this.subscribeToTypeChanges(); merge( this.latestKeyFormControl.valueChanges, this.entityKeyFormControl.valueChanges, @@ -150,43 +148,25 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali const errors: any = {}; if (this.limitForm.invalid) { - errors.form = this.getFormErrors(); + errors.form = false; } if (type === ValueSourceType.latestKey) { if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { - errors.latestKey = { - valid: false - }; + errors.latestKey = false; } } else if (type === ValueSourceType.entity) { if (!this.limitForm.get('entityAlias')?.value) { - errors.entityAlias = { - valid: false - }; + errors.entityAlias = false; } if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { - errors.entityKey = { - valid: false - }; + errors.entityKey = false; } } return Object.keys(errors).length ? { axisLimitForm: errors } : null; } - - private getFormErrors(): any { - const errors: any = {}; - Object.keys(this.limitForm.controls).forEach(key => { - const control = this.limitForm.get(key); - if (control && control.errors) { - errors[key] = control.errors; - } - }); - return errors; - } - private updateValidators() { const axisTypeControl = this.limitForm.get('type'); if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { @@ -207,38 +187,23 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } } - private subscribeToTypeChanges() { - this.limitForm.controls.type.valueChanges - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - const axisValueControl = this.limitForm.get('value'); - const axisEntityAliasControl = this.limitForm.get('entityAlias'); - if (axisValueControl) { - axisValueControl.setValue(null, { emitEvent: false }); - } - if (axisEntityAliasControl) { - axisEntityAliasControl.setValue(null, { emitEvent: false }); - } - this.latestKeyFormControl.setValue(null, { emitEvent: false }); - this.entityKeyFormControl.setValue(null, { emitEvent: false }); - - }); - } - private updateModel() { const value = this.limitForm.value; - this.modelValue.type = value.type ?? ValueSourceType.constant; - this.modelValue.value = value?.value; - this.modelValue.entityAlias = value?.entityAlias; - if (value.type === ValueSourceType.latestKey) { + const type = value.type; + let updates: Partial = { type }; + if (type === ValueSourceType.latestKey) { const latestKey: DataKey = this.latestKeyFormControl.value; - this.modelValue.latestKey = latestKey?.name; - this.modelValue.latestKeyType = (latestKey?.type as any); - } else if (value.type === ValueSourceType.entity) { + updates.latestKey = latestKey?.name; + updates.latestKeyType = latestKey?.type as any; + } else if (type === ValueSourceType.entity) { const entityKey: DataKey = this.entityKeyFormControl.value; - this.modelValue.entityKey = entityKey?.name; - this.modelValue.entityKeyType = (entityKey?.type as any); + updates.entityKey = entityKey?.name; + updates.entityKeyType = entityKey?.type as any; + updates.entityAlias = value?.entityAlias; + } else { + updates.value = value?.value; } + this.modelValue = updates as ValueSourceConfig; this.propagateChanges(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 4bf7a78f98..9bed5d6e7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -168,10 +168,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu validate(): ValidationErrors | null { return this.axisSettingsFormGroup.valid ? null : { - axisSettings: { - valid: false, - errors: this.getFormErrors() - } + axisSettings: false }; } @@ -250,17 +247,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu } } - private getFormErrors(): any { - const errors: any = {}; - Object.keys(this.axisSettingsFormGroup.controls).forEach(key => { - const control = this.axisSettingsFormGroup.get(key); - if (control && control.errors) { - errors[key] = control.errors; - } - }); - return errors; - } - private updateModel() { this.modelValue = this.axisSettingsFormGroup.getRawValue(); this.propagateChange(this.modelValue); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 3159a27abd..4d710a6ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -196,6 +196,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; axis.id = getNextTimeSeriesYAxisId(axes); axis.order = axes.length; + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); axesArray.push(axisControl); @@ -204,8 +206,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { const axesControls: Array = []; axes.forEach((axis) => { - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); }); return this.fb.array(axesControls); @@ -216,17 +216,20 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const result: TimeSeriesChartYAxes = {}; for (const [id, axis] of Object.entries(yAxes)) { + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; - const minCfg = axis.min as unknown as ValueSourceConfig; - const maxCfg = axis.max as unknown as ValueSourceConfig; - - const minValid = - minCfg?.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, minCfg)); + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, minCfg)) + ); - const maxValid = - maxCfg?.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, maxCfg)); + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, maxCfg)) + ); if (minValid && maxValid) { result[id] = axis; @@ -288,17 +291,17 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, d.type === limit.latestKeyType); } - private normalizeAxisLimit(limit: any): ValueSourceConfig { + private normalizeAxisLimit(limit: string | number | ValueSourceConfig): ValueSourceConfig { if (!limit) { return { type: ValueSourceType.constant, value: null, entityAlias: null }; - } else if (typeof limit === 'number') { + } else if (typeof limit === 'number' || typeof limit === 'string') { return { type: ValueSourceType.constant, - value: limit, + value: Number(limit), entityAlias: null }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 420e834106..6389bbd6f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -30,14 +30,16 @@ -
+
- +
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 1857852afe..6f4c618394 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -43,6 +43,7 @@ import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -99,8 +100,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O position: [null, []], units: [null, []], decimals: [null, []], - min: [null, []], - max: [null, []], + min: this.createLimitFormGroup(), + max: this.createLimitFormGroup(), show: [null, []] }); this.axisFormGroup.valueChanges.pipe( @@ -195,6 +196,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } } + checkIsConstantLimit(limit: 'min' | 'max') { + return this.axisFormGroup.get(`${limit}.type`)?.value === ValueSourceType.constant; + } + private updateValidators() { const show: boolean = this.axisFormGroup.get('show').value; if (show) { @@ -208,6 +213,13 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.axisFormGroup.get('units').disable({emitEvent: false}); this.axisFormGroup.get('decimals').disable({emitEvent: false}); } + if(!this.checkIsConstantLimit('min')){ + this.axisFormGroup.get('min').disable({emitEvent: false}); + } + if(!this.checkIsConstantLimit('max')){ + this.axisFormGroup.get('max').disable({emitEvent: false}); + } + } private updateModel() { @@ -221,4 +233,12 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.modelValue.show = value.show; this.propagateChange(this.modelValue); } + + private createLimitFormGroup() { + return this.fb.group({ + type: [ValueSourceType.constant, []], + value: [null, []], + entityAlias: [null, []] + }) + } } From 846deebb43c282d0ca5cd0d1ccfb3efa9e9009b1 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 9 Dec 2025 12:11:05 +0200 Subject: [PATCH 049/147] Minor changes --- .../server/dao/notification/DefaultNotifications.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 7e82ae9472..5e5092eb5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -84,7 +84,7 @@ public class DefaultNotifications { .build(); public static final DefaultNotification entitiesLimitForSysadmin = DefaultNotification.builder() - .name("Entities count limit notification for sysadmin") + .name("Entities count limit notification to sysadmin") .type(NotificationType.ENTITIES_LIMIT) .subject("${entityType}s limit will be reached soon for tenant ${tenantName}") .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)") From 2c04943984abebdd7fe07772046e20d2173e8531 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 9 Dec 2025 12:11:14 +0200 Subject: [PATCH 050/147] removed checks for 0 value --- .../server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 9d23bdbcb5..46bc10010e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -51,9 +51,7 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return cached; } var compositeKey = new KeyDictionaryCompositeKey(strKey); - Optional existingId = keyDictionaryRepository.findById(compositeKey) - .map(KeyDictionaryEntry::getKeyId) - .filter(id -> id != 0); + Optional existingId = keyDictionaryRepository.findById(compositeKey).map(KeyDictionaryEntry::getKeyId); if (existingId.isPresent()) { return cacheAndReturn(strKey, existingId.get()); } @@ -64,13 +62,12 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService return fromCache; } Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey); - if (keyId != null && keyId != 0) { + if (keyId != null) { return cacheAndReturn(strKey, keyId); } log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey); keyId = keyDictionaryRepository.findById(compositeKey) .map(KeyDictionaryEntry::getKeyId) - .filter(id -> id != 0) .orElseThrow(() -> new IllegalStateException( "Failed to resolve keyId for string key: " + strKey + " after fallback.")); return cacheAndReturn(strKey, keyId); From 793660cb721a508e2edb156655077e68a8d1e607 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 9 Dec 2025 12:48:08 +0200 Subject: [PATCH 051/147] Add test for JavaRestClient --- .../msa/connectivity/JavaRestClientTest.java | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index fb41cdf77f..710bc103c6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -76,6 +76,8 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -92,6 +94,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -318,7 +321,7 @@ public class JavaRestClientTest extends AbstractContainerTest { Domain savedDomain = restClient.saveDomain(domain); assertThat(savedDomain.getName()).isEqualTo(domain.getName()); - PageData domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0 , prefix)); + PageData domainInfos = restClient.getTenantDomainInfos(new PageLink(10, 0, prefix)); assertThat(domainInfos.getData()).hasSize(1); } @@ -362,7 +365,7 @@ public class JavaRestClientTest extends AbstractContainerTest { } private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, - String text, NotificationDeliveryMethod... deliveryMethods) { + String text, NotificationDeliveryMethod... deliveryMethods) { NotificationTemplate notificationTemplate = new NotificationTemplate(); notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5)); notificationTemplate.setNotificationType(notificationType); @@ -410,4 +413,53 @@ public class JavaRestClientTest extends AbstractContainerTest { .build(); return restClient.saveNotificationRequest(notificationRequest); } + + @Test + public void testApiKeyOperations() { + // Create an API key + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API Key " + RandomStringUtils.randomAlphabetic(5)); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(user.getId()); + apiKeyInfo.setExpirationTime(0); + + ApiKey savedApiKey = restClient.saveApiKey(apiKeyInfo); + assertThat(savedApiKey).isNotNull(); + assertThat(savedApiKey.getId()).isNotNull(); + assertThat(savedApiKey.getDescription()).isEqualTo(apiKeyInfo.getDescription()); + assertThat(savedApiKey.isEnabled()).isTrue(); + assertThat(savedApiKey.getUserId()).isEqualTo(user.getId()); + assertThat(savedApiKey.getTenantId()).isEqualTo(tenant.getId()); + assertThat(savedApiKey.getValue()).isNotNull(); + + // Get user API keys + PageData apiKeys = restClient.getUserApiKeys(user.getId(), new PageLink(10)); + assertThat(apiKeys).isNotNull(); + assertThat(apiKeys.getData()).hasSize(1); + assertThat(apiKeys.getData().get(0).getId()).isEqualTo(savedApiKey.getId()); + + // Update API key description + String updatedDescription = "Updated description " + RandomStringUtils.randomAlphabetic(5); + ApiKeyInfo updatedApiKeyInfo = restClient.updateApiKeyDescription(savedApiKey.getId(), Optional.of(updatedDescription)); + assertThat(updatedApiKeyInfo).isNotNull(); + assertThat(updatedApiKeyInfo.getDescription()).isEqualTo(updatedDescription); + + // Disable the API key + ApiKeyInfo disabledApiKey = restClient.enableApiKey(savedApiKey.getId(), false); + assertThat(disabledApiKey).isNotNull(); + assertThat(disabledApiKey.isEnabled()).isFalse(); + + // Enable the API key + ApiKeyInfo enabledApiKey = restClient.enableApiKey(savedApiKey.getId(), true); + assertThat(enabledApiKey).isNotNull(); + assertThat(enabledApiKey.isEnabled()).isTrue(); + + // Delete the API key + restClient.deleteApiKey(savedApiKey.getId()); + + // Verify the API key is deleted + PageData apiKeysAfterDelete = restClient.getUserApiKeys(user.getId(), new PageLink(10)); + assertThat(apiKeysAfterDelete.getData()).isEmpty(); + } + } From a2f196dc39fbf667dbe4f84b8ecb9acf85cac440 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 9 Dec 2025 13:10:41 +0200 Subject: [PATCH 052/147] Minor changes --- .../server/msa/connectivity/JavaRestClientTest.java | 8 ++++---- .../main/java/org/thingsboard/rest/client/RestClient.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 710bc103c6..5b98f4859b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -94,8 +94,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.EMAIL; @@ -357,7 +357,7 @@ public class JavaRestClientTest extends AbstractContainerTest { filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); NotificationTarget notificationTarget = new NotificationTarget(); - notificationTarget.setName(filter.toString() + org.apache.commons.lang3.RandomStringUtils.randomNumeric(5)); + notificationTarget.setName(filter + RandomStringUtils.randomNumeric(5)); PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); targetConfig.setUsersFilter(filter); notificationTarget.setConfiguration(targetConfig); @@ -407,7 +407,7 @@ public class JavaRestClientTest extends AbstractContainerTest { NotificationRequestConfig config = new NotificationRequestConfig(); config.setSendingDelayInSec(0); NotificationRequest notificationRequest = NotificationRequest.builder() - .targets(List.of(targetId).stream().map(UUIDBased::getId).collect(Collectors.toList())) + .targets(Stream.of(targetId).map(UUIDBased::getId).collect(Collectors.toList())) .templateId(notificationTemplateId) .additionalConfig(config) .build(); @@ -440,7 +440,7 @@ public class JavaRestClientTest extends AbstractContainerTest { // Update API key description String updatedDescription = "Updated description " + RandomStringUtils.randomAlphabetic(5); - ApiKeyInfo updatedApiKeyInfo = restClient.updateApiKeyDescription(savedApiKey.getId(), Optional.of(updatedDescription)); + ApiKeyInfo updatedApiKeyInfo = restClient.updateApiKeyDescription(savedApiKey.getId(), updatedDescription); assertThat(updatedApiKeyInfo).isNotNull(); assertThat(updatedApiKeyInfo.getDescription()).isEqualTo(updatedDescription); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index d4781c981a..7686949980 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -3065,7 +3065,7 @@ public class RestClient implements Closeable { new ParameterizedTypeReference>() {}, params).getBody(); } - public ApiKeyInfo updateApiKeyDescription(ApiKeyId apiKeyId, Optional description) { + public ApiKeyInfo updateApiKeyDescription(ApiKeyId apiKeyId, String description) { return restTemplate.exchange( baseURL + "/api/apiKey/{id}/description", HttpMethod.PUT, From a44d2618a1886557288eee6c0947248e671d9656 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 9 Dec 2025 13:41:41 +0200 Subject: [PATCH 053/147] Update notification template --- .../server/dao/notification/DefaultNotifications.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 5e5092eb5b..32efe64d25 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -106,7 +106,7 @@ public class DefaultNotifications { .build()) .build(); public static final DefaultNotification entitiesLimitIncreaseRequest = DefaultNotification.builder() - .name("Entities limit increase request for sysadmin") + .name("Entities limit increase request") .type(NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) .subject("${entityType} limit increase request") .text("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.") From 4c9189023c546095164d3212401bcc157815ee32 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 9 Dec 2025 13:56:51 +0200 Subject: [PATCH 054/147] Revert --- .../server/dao/notification/DefaultNotifications.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 32efe64d25..920fd8c144 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -84,7 +84,7 @@ public class DefaultNotifications { .build(); public static final DefaultNotification entitiesLimitForSysadmin = DefaultNotification.builder() - .name("Entities count limit notification to sysadmin") + .name("Entities count limit notification for sysadmin") .type(NotificationType.ENTITIES_LIMIT) .subject("${entityType}s limit will be reached soon for tenant ${tenantName}") .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)") From c00ddbea2fcea62dc076f567a98bb7a6f4251300 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 9 Dec 2025 14:03:39 +0200 Subject: [PATCH 055/147] Updated to be in sync with PE --- .../server/service/edge/rpc/EdgeSyncCursor.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 20de7186b9..a8960abf0b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -65,6 +65,12 @@ public class EdgeSyncCursor { fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); + fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); + fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); + fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); + fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); + fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); + fetchers.add(new AiModelEdgeEventFetcher(ctx.getAiModelService())); } Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId()); if (publicCustomer != null) { @@ -85,15 +91,9 @@ public class EdgeSyncCursor { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService())); - fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); - fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService())); - fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); - fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); - fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); - fetchers.add(new AiModelEdgeEventFetcher(ctx.getAiModelService())); } } From dd7972721aa0db87ab03e2f902ec6d9e92e891c9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 15:31:55 +0200 Subject: [PATCH 056/147] fixed errors --- .../widget/lib/chart/time-series-chart.ts | 10 ++-- ...eries-chart-widget-settings.component.html | 3 + .../common/axis-scale-row.component.html | 2 +- ...ime-series-chart-y-axes-panel.component.ts | 2 + .../time-series-chart-y-axis-row.component.ts | 60 ++++++++++++++----- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 8568e4b13b..9989df42e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -307,10 +307,12 @@ export class TbTimeSeriesChart { const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); if (data?.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - if (yAxis.option.max !== value) { - yAxis.option.max = value; - update = true; - } + // if (yAxis.option.max !== value) { + // yAxis.option.max = value; + // update = true; + // } + update = true; + yAxis.option.max = 124; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 9a80362c8d..afa3c774ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -62,6 +62,9 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html index 37c2838a21..0129bdc490 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -35,7 +35,7 @@ formControlName="entityAlias">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 4d710a6ff4..690ad6cc55 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -250,6 +250,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const foundYAxisKeys: DataKey[] = []; for(const yAxis of yAxes) { + console.log("yAxis",yAxis) + const min = yAxis.min as ValueSourceConfig; const max = yAxis.max as ValueSourceConfig; if (min.type === ValueSourceType.latestKey) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 6f4c618394..34f1b487a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -135,17 +135,19 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - this.axisFormGroup.patchValue( - { - label: value.label, - position: value.position, - units: value.units, - decimals: value.decimals, - min: value.min, - max: value.max, - show: value.show, - }, {emitEvent: false} - ); + const min = this.normalizeLimit(value.min); + const max = this.normalizeLimit(value.max); + + this.axisFormGroup.patchValue({ + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min, + max, + show: value.show, + }, { emitEvent: false }); + this.updateValidators(); this.cd.markForCheck(); } @@ -223,7 +225,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } private updateModel() { - const value = this.axisFormGroup.value; + const value = this.axisFormGroup.getRawValue(); + console.log("value", value); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; @@ -238,7 +241,36 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O return this.fb.group({ type: [ValueSourceType.constant, []], value: [null, []], - entityAlias: [null, []] - }) + latestKey: [null, []], + latestKeyType: [null, []], + entityAlias: [null, []], + entityKey: [null, []], + entityKeyType: [null, []] + }); + } + + private normalizeLimit(limit: any) { + const base = { + type: ValueSourceType.constant, + value: null, + latestKey: null, + latestKeyType: null, + entityAlias: null, + entityKey: null, + entityKeyType: null + }; + + if (limit == null) return base; + + if (typeof limit === 'number' || typeof limit === 'string') { + return { ...base, type: ValueSourceType.constant, value: Number(limit) }; + } + + return { + ...base, + ...limit, + }; } + + } From 551574af3b438681322d9f6c3a3e07793a3e8b28 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 9 Dec 2025 16:19:12 +0200 Subject: [PATCH 057/147] Improve 'Entities Limit Exceeded' Error and Notification Template. --- .../server/controller/BaseController.java | 4 +- ...ngsboardEntitiesLimitExceededResponse.java | 50 +++++++++++++++++++ .../exception/ThingsboardErrorResponse.java | 31 +----------- .../ThingsboardErrorResponseHandler.java | 23 ++++----- .../transport/DefaultTransportApiService.java | 4 +- ...va => EntitiesLimitExceededException.java} | 4 +- .../notification/DefaultNotifications.java | 25 +++++++++- .../server/dao/service/DataValidator.java | 4 +- ui-ngx/src/app/shared/models/constants.ts | 1 + .../assets/locale/locale.constant-en_US.json | 3 +- 10 files changed, 98 insertions(+), 51 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java rename dao/src/main/java/org/thingsboard/server/dao/exception/{EntitiesLimitException.java => EntitiesLimitExceededException.java} (87%) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index a58ae911a2..83603f7ea5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -151,7 +151,7 @@ import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; @@ -453,7 +453,7 @@ public abstract class BaseController { } if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; - } else if (exception instanceof EntitiesLimitException) { + } else if (exception instanceof EntitiesLimitExceededException) { return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED); } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java new file mode 100644 index 0000000000..c1faabb569 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +@Schema +public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse { + + private final EntityType entityType; + + private final Long limit; + + protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) { + super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN); + this.entityType = entityType; + this.limit = limit; + } + + public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) { + return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit); + } + + @Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY) + public EntityType getEntityType() { + return entityType; + } + + @Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY) + public Long getLimit() { + return limit; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index f38b86e394..8a564dedca 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -15,10 +15,8 @@ */ package org.thingsboard.server.exception; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.http.HttpStatus; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @Schema @@ -34,19 +32,9 @@ public class ThingsboardErrorResponse { private final long timestamp; - private EntityType entityType; - - private Long limit; - protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, HttpStatus status) { - this(message, errorCode, null, null, status); - } - - protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, EntityType entityType, Long limit, HttpStatus status) { this.message = message; this.errorCode = errorCode; - this.entityType = entityType; - this.limit = limit; this.status = status; this.timestamp = System.currentTimeMillis(); } @@ -55,14 +43,6 @@ public class ThingsboardErrorResponse { return new ThingsboardErrorResponse(message, errorCode, status); } - public static ThingsboardErrorResponse ofEntityLimitExceeded(final String message, - EntityType entityType, - Long limit, - HttpStatus status) { - return new ThingsboardErrorResponse(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, - entityType, limit, status); - } - @Schema(description = "HTTP Response Status Code", example = "401", accessMode = Schema.AccessMode.READ_ONLY) public Integer getStatus() { return status.value(); @@ -84,7 +64,8 @@ public class ThingsboardErrorResponse { "\n\n* `32` - Item not found (HTTP: 404 - Not Found)" + "\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" + "\n\n* `34` - Too many updates (Too many updates over Websocket session)" + - "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)", + "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)" + + "\n\n* `41` - Entities limit exceeded (HTTP: 403 - Forbidden)", example = "10", type = "integer", accessMode = Schema.AccessMode.READ_ONLY) public ThingsboardErrorCode getErrorCode() { @@ -95,12 +76,4 @@ public class ThingsboardErrorResponse { public long getTimestamp() { return timestamp; } - - public EntityType getEntityType() { - return entityType; - } - - public Long getLimit() { - return limit; - } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index e6e27e6a75..3ef24e2db9 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -52,7 +52,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; @@ -147,10 +147,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) { handleDatabaseException(thingsboardException.getCause(), response); } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) { - if (thingsboardException.getCause() instanceof EntitiesLimitException entitiesLimitException) { - handleEntitiesLimitException(entitiesLimitException, response); + if (thingsboardException.getCause() instanceof EntitiesLimitExceededException entitiesLimitExceededException) { + handleEntitiesLimitExceededException(entitiesLimitExceededException, response); } else { - handleEntitiesLimitException(thingsboardException, response); + handleEntitiesLimitExceededException(thingsboardException, response); } } else { handleThingsboardException(thingsboardException, response); @@ -227,19 +227,18 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand writeResponse(errorResponse, response); } - private void handleEntitiesLimitException(ThingsboardException entitiesLimitException, HttpServletResponse response) throws IOException { + private void handleEntitiesLimitExceededException(ThingsboardException entitiesLimitExceededException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), - JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitException.getCause()).getResponseBodyAsByteArray(), Object.class)); + JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitExceededException.getCause()).getResponseBodyAsByteArray(), Object.class)); } - private void handleEntitiesLimitException(EntitiesLimitException entitiesLimitException, HttpServletResponse response) throws IOException { - EntityType entityType = entitiesLimitException.getEntityType(); - Long limit = entitiesLimitException.getLimit(); - HttpStatus status = HttpStatus.FORBIDDEN; - response.setStatus(status.value()); + private void handleEntitiesLimitExceededException(EntitiesLimitExceededException entitiesLimitExceededException, HttpServletResponse response) throws IOException { + EntityType entityType = entitiesLimitExceededException.getEntityType(); + Long limit = entitiesLimitExceededException.getLimit(); + response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), - ThingsboardErrorResponse.ofEntityLimitExceeded(entitiesLimitException.getMessage(), entityType, limit, status)); + ThingsboardEntitiesLimitExceededResponse.of(entitiesLimitExceededException.getMessage(), entityType, limit)); } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 7fee411704..096c1a814a 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -76,7 +76,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -398,7 +398,7 @@ public class DefaultTransportApiService implements TransportApiService { } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); - } catch (EntitiesLimitException e) { + } catch (EntitiesLimitExceededException e) { log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage()); return TransportApiResponseMsg.newBuilder() .setGetOrCreateDeviceResponseMsg( diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java similarity index 87% rename from dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java rename to dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java index 559ef94e45..b2907c9104 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java @@ -19,7 +19,7 @@ import lombok.Getter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -public class EntitiesLimitException extends DataValidationException { +public class EntitiesLimitExceededException extends DataValidationException { private static final long serialVersionUID = -9211462514373279196L; @Getter @@ -30,7 +30,7 @@ public class EntitiesLimitException extends DataValidationException { @Getter private final long limit; - public EntitiesLimitException(TenantId tenantId, EntityType entityType, long limit) { + public EntitiesLimitExceededException(TenantId tenantId, EntityType entityType, long limit) { super(entityType.getNormalName() + "s limit reached"); this.tenantId = tenantId; this.entityType = entityType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 920fd8c144..e305ca556c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -113,7 +113,30 @@ public class DefaultNotifications { .button("${increaseLimitActionLabel}").link("${increaseLimitLink}") .emailTemplate(DefaultEmailTemplate.builder() .subject("${entityType} limit increase request") - .body("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
${increaseLimitActionLabel}") + .body(""" + + + + + + +
+ + + + + + + + + + + + +
${entityType} limit increase request
${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
+ ${increaseLimitActionLabel} +
+
""") .build()) .build(); public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder() diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index ffa88fe39c..89ab237ed3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitException; +import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -124,7 +124,7 @@ public abstract class DataValidator> { EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { long limit = apiLimitService.getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType)); - throw new EntitiesLimitException(tenantId, entityType, limit); + throw new EntitiesLimitExceededException(tenantId, entityType, limit); } } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 72315e8ed5..2788aa52df 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -53,6 +53,7 @@ export const serverErrorCodesTranslations = new Map([ [Constants.serverErrorCode.itemNotFound, 'server-error.item-not-found'], [Constants.serverErrorCode.tooManyRequests, 'server-error.too-many-requests'], [Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'], + [Constants.serverErrorCode.entitiesLimitExceeded, 'server-error.entities-limit-exceeded'], ]); export const MediaBreakpoints = { 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 7a7b5bff62..27bd757078 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6140,7 +6140,8 @@ "bad-request-params": "Bad request params", "item-not-found": "Item not found", "too-many-requests": "Too many requests", - "too-many-updates": "Too many updates" + "too-many-updates": "Too many updates", + "entities-limit-exceeded": "Entities limit exceeded" }, "tenant": { "tenant": "Tenant", From e68c0012cb6d966f3789d24865121ca6dd87bc20 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 9 Dec 2025 16:35:43 +0200 Subject: [PATCH 058/147] added null check for output --- .../service/cf/ctx/state/CalculatedFieldCtx.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 7f515a5c9a..b97be4dcd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -637,10 +637,12 @@ public class CalculatedFieldCtx implements Closeable { } public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation - var thisOutputStrategy = calculatedField.getConfiguration().getOutput().getStrategy(); - var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); - if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) { - return true; + if (output != null) { + var thisOutputStrategy = output.getStrategy(); + var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy(); + if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) { + return true; + } } if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig @@ -657,7 +659,7 @@ public class CalculatedFieldCtx implements Closeable { if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { return true; } - if (output.hasContextOnlyChanges(other.output)) { + if (output != null && output.hasContextOnlyChanges(other.output)) { return true; } if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig From e8b164a03ad2e07fa3c8ecc61daf14e70d0ed7dc Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 9 Dec 2025 16:56:32 +0200 Subject: [PATCH 059/147] Refactoring. Remove EdgeStats - uplinkRate not used --- .../service/edge/EdgeContextComponent.java | 1 - .../service/edge/rpc/EdgeGrpcSession.java | 6 ++-- .../service/edge/stats/EdgeStatsService.java | 14 ++++---- .../server/edge/EdgeStatsIntegrationTest.java | 12 +++---- .../server/service/edge/EdgeStatsTest.java | 25 +++++++------- .../server/dao/edge/stats/EdgeStats.java | 34 ------------------- .../edge/stats/EdgeStatsCounterService.java | 13 ++++--- 7 files changed, 34 insertions(+), 71 deletions(-) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index ec4a43310b..d523a9176c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -206,7 +206,6 @@ public class EdgeContextComponent { @Autowired private Optional statsCounterService; - // processors @Autowired private AlarmProcessor alarmProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index c0e76c32e4..83639573bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -306,7 +306,8 @@ public abstract class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { if (fetcher instanceof GeneralEdgeEventFetcher) { long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize)); } log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); @@ -504,7 +505,8 @@ public abstract class EdgeGrpcSession implements Closeable { ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); stopCurrentSendDownlinkMsgsTask(false); } } else { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 697f7b6216..66be8fe32a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.dao.edge.stats.EdgeStats; import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -81,11 +80,10 @@ public class EdgeStatsService { long now = System.currentTimeMillis(); long ts = now - (now % reportIntervalMillis); - Map statsByEdgeSnapshot = new HashMap<>(statsCounterService.getStatsByEdge()); + Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getMsgCountersByEdge()); boolean isKafkaStats = kafkaAdmin.isPresent(); - Map lagByEdgeId = isKafkaStats ? getLagByEdgeId(statsByEdgeSnapshot) : Collections.emptyMap(); - statsByEdgeSnapshot.forEach((edgeId, edgeStats) -> { - MsgCounters counters = edgeStats.getMsgCounters(); + Map lagByEdgeId = isKafkaStats ? getLagByEdgeId(countersByEdgeSnapshot) : Collections.emptyMap(); + countersByEdgeSnapshot.forEach((edgeId, counters) -> { TenantId tenantId = counters.getTenantId(); if (isKafkaStats) { @@ -104,11 +102,11 @@ public class EdgeStatsService { }); } - private Map getLagByEdgeId(Map edgeStatsByEdge) { - Map edgeToTopicMap = edgeStatsByEdge.entrySet().stream() + private Map getLagByEdgeId(Map countersByEdge) { + Map edgeToTopicMap = countersByEdge.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, - e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getMsgCounters().getTenantId(), e.getKey()).getTopic() + e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getTenantId(), e.getKey()).getTopic() )); Map lagByTopic = kafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values())); diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java index b993e4f564..ed98ebe16d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -179,19 +179,19 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { private void prepareTestData() throws InterruptedException, ExecutionException { statsCounterService.clear(edge.getId()); - // 2 stats message ADDED Device Profile, ASSIGN Device + // 2 stats messages: ADDED Device Profile, ASSIGN Device edgeImitator.expectMessageAmount(4); Device device = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); doPost("/api/edge/" + edge.getUuidId() + "/device/" + device.getUuidId(), Device.class); edgeImitator.waitForMessages(); - // 1 stats message ASSIGN Asset + // 1 stats message: ASSIGN Asset edgeImitator.expectMessageAmount(2); Asset savedAsset = saveAsset("Edge Asset 2"); doPost("/api/edge/" + edge.getUuidId() + "/asset/" + savedAsset.getUuidId(), Asset.class); Assert.assertTrue(edgeImitator.waitForMessages()); - // 2 stats message ADDED Customer, ASSIGN Customer + // 2 stats messages: ADDED Customer, ASSIGN Customer edgeImitator.expectMessageAmount(1); Customer customer = new Customer(); customer.setTitle("Edge Customer"); @@ -204,7 +204,7 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { + "/edge/" + edge.getUuidId(), Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); - //1 stats message Timeseries + // 1 stats message: Timeseries edgeImitator.expectMessageAmount(1); String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); @@ -222,7 +222,7 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { private void awaitEdgeCountersUpdated() { await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { - MsgCounters counters = statsCounterService.getStatsByEdge().get(edge.getId()).getMsgCounters(); + MsgCounters counters = statsCounterService.getMsgCountersByEdge().get(edge.getId()); Map actualCounters = toMap( Map.entry(DOWNLINK_MSGS_ADDED.getKey(), () -> counters.getMsgsAdded().get()), Map.entry(DOWNLINK_MSGS_PUSHED.getKey(), () -> counters.getMsgsPushed().get()), @@ -234,7 +234,7 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { } @SafeVarargs - private final Map toMap(Map.Entry>... suppliers) { + private Map toMap(Map.Entry>... suppliers) { return Arrays.stream(suppliers) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index fb24cdb5bb..0b0f28e923 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -29,7 +30,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.edge.stats.EdgeStats; import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -72,6 +72,9 @@ public class EdgeStatsTest { private EdgeStatsCounterService statsCounterService; private EdgeStatsService edgeStatsService; + @Captor + private ArgumentCaptor> captor; + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); private final EdgeId edgeId = new EdgeId(UUID.randomUUID()); @@ -94,20 +97,18 @@ public class EdgeStatsTest { @Test public void testReportStatsSavesTelemetry() { - EdgeStats edgeStats = new EdgeStats(tenantId); - MsgCounters counters = edgeStats.getMsgCounters(); + MsgCounters counters = new MsgCounters(tenantId); counters.getMsgsAdded().set(5); counters.getMsgsPushed().set(3); counters.getMsgsPermanentlyFailed().set(1); counters.getMsgsTmpFailed().set(0); counters.getMsgsLag().set(10); - ConcurrentHashMap edgeStatsByEdge = new ConcurrentHashMap<>(); - edgeStatsByEdge.put(edgeId, edgeStats); + ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); + countersByEdge.put(edgeId, counters); - when(statsCounterService.getStatsByEdge()).thenReturn(edgeStatsByEdge); + when(statsCounterService.getMsgCountersByEdge()).thenReturn(countersByEdge); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class) List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); @@ -131,18 +132,17 @@ public class EdgeStatsTest { @Test public void testReportStatsWithKafkaLag() { - EdgeStats edgeStats = new EdgeStats(tenantId); - MsgCounters counters = edgeStats.getMsgCounters(); + MsgCounters counters = new MsgCounters(tenantId); counters.getMsgsAdded().set(2); counters.getMsgsPushed().set(2); counters.getMsgsPermanentlyFailed().set(0); counters.getMsgsTmpFailed().set(1); counters.getMsgsLag().set(0); - ConcurrentHashMap edgeStatsByEdge = new ConcurrentHashMap<>(); - edgeStatsByEdge.put(edgeId, edgeStats); + ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); + countersByEdge.put(edgeId, counters); - when(statsCounterService.getStatsByEdge()).thenReturn(edgeStatsByEdge); + when(statsCounterService.getMsgCountersByEdge()).thenReturn(countersByEdge); String topic = "edge-topic"; TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); @@ -152,7 +152,6 @@ public class EdgeStatsTest { when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) .thenReturn(Map.of(topic, 15L)); - ArgumentCaptor> captor = ArgumentCaptor.forClass((Class) List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java deleted file mode 100644 index 36d87691cc..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStats.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.edge.stats; - -import lombok.Data; -import org.thingsboard.server.common.data.id.TenantId; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -@Data -public class EdgeStats { - private final MsgCounters msgCounters; - private final Queue uplinkRate; - - public EdgeStats(TenantId tenantId) { - this.msgCounters = new MsgCounters(tenantId); - this.uplinkRate = new ConcurrentLinkedQueue<>(); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java index c537fb75be..ea8b85a287 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java @@ -24,17 +24,16 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.concurrent.ConcurrentHashMap; -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @Service @Slf4j @Getter public class EdgeStatsCounterService { - private final ConcurrentHashMap statsByEdge = new ConcurrentHashMap<>(); + private final ConcurrentHashMap msgCountersByEdge = new ConcurrentHashMap<>(); public void recordEvent(EdgeStatsKey type, TenantId tenantId, EdgeId edgeId, long value) { - EdgeStats edgeStats = getOrCreateEdgeStats(tenantId, edgeId); - MsgCounters counters = edgeStats.getMsgCounters(); + MsgCounters counters = getOrCreateCounters(tenantId, edgeId); switch (type) { case DOWNLINK_MSGS_ADDED -> counters.getMsgsAdded().addAndGet(value); case DOWNLINK_MSGS_PUSHED -> counters.getMsgsPushed().addAndGet(value); @@ -44,12 +43,12 @@ public class EdgeStatsCounterService { } } - public EdgeStats getOrCreateEdgeStats(TenantId tenantId, EdgeId edgeId) { - return statsByEdge.computeIfAbsent(edgeId, id -> new EdgeStats(tenantId)); + public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { + return msgCountersByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); } public void clear(EdgeId edgeId) { - statsByEdge.remove(edgeId); + msgCountersByEdge.remove(edgeId); } } From 631a580211360669312a194f935ff1ead0c0ef1f Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 17:40:30 +0200 Subject: [PATCH 060/147] cleanup --- .../components/widget/lib/chart/time-series-chart.ts | 10 ++++------ .../chart/time-series-chart-y-axes-panel.component.ts | 2 -- .../chart/time-series-chart-y-axis-row.component.ts | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 9989df42e8..8568e4b13b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -307,12 +307,10 @@ export class TbTimeSeriesChart { const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); if (data?.data[0]) { const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); - // if (yAxis.option.max !== value) { - // yAxis.option.max = value; - // update = true; - // } - update = true; - yAxis.option.max = 124; + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 690ad6cc55..4d710a6ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -250,8 +250,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const foundYAxisKeys: DataKey[] = []; for(const yAxis of yAxes) { - console.log("yAxis",yAxis) - const min = yAxis.min as ValueSourceConfig; const max = yAxis.max as ValueSourceConfig; if (min.type === ValueSourceType.latestKey) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 34f1b487a2..46c7c989c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -226,7 +226,6 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O private updateModel() { const value = this.axisFormGroup.getRawValue(); - console.log("value", value); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; From e378c2b7952f21883dc7f81c8dde0a68a77115a9 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 9 Dec 2025 17:51:56 +0200 Subject: [PATCH 061/147] enhanced validators --- .../chart/time-series-chart-y-axis-row.component.html | 6 ++---- .../common/chart/time-series-chart-y-axis-row.component.ts | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 6389bbd6f8..22d3fab103 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -32,14 +32,12 @@
- +
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 46c7c989c0..83f1b6623a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -217,9 +217,13 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } if(!this.checkIsConstantLimit('min')){ this.axisFormGroup.get('min').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('min').enable({emitEvent: false}); } if(!this.checkIsConstantLimit('max')){ this.axisFormGroup.get('max').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('max').enable({emitEvent: false}); } } @@ -270,6 +274,4 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O ...limit, }; } - - } From c42705c03271be54c04893c57997518a60c72aad Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 10 Dec 2025 10:04:26 +0200 Subject: [PATCH 062/147] Simplify tests --- .../service/edge/rpc/EdgeGrpcSession.java | 6 +- .../edge/rpc/KafkaEdgeEventService.java | 3 +- .../service/edge/stats/EdgeStatsService.java | 3 +- .../server/edge/EdgeStatsIntegrationTest.java | 237 ++++-------------- .../server/service/edge/EdgeStatsTest.java | 88 ++++--- .../dao/edge/PostgresEdgeEventService.java | 3 +- 6 files changed, 102 insertions(+), 238 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 83639573bb..668c698376 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -546,7 +546,8 @@ public abstract class EdgeGrpcSession implements Closeable { try { if (msg.getSuccess()) { sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); } else { log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); @@ -815,7 +816,8 @@ public abstract class EdgeGrpcSession implements Closeable { } } highPriorityQueue.add(edgeEvent); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); } protected ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java index bc00ef4481..b4f7a7574f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java @@ -52,7 +52,8 @@ public class KafkaEdgeEventService extends BaseEdgeEventService { TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build(); producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); return Futures.immediateFuture(null); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 66be8fe32a..7f55d2817c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -54,7 +54,7 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_P import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; @TbCoreComponent -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @RequiredArgsConstructor @Service @Slf4j @@ -70,7 +70,6 @@ public class EdgeStatsService { @Value("${edges.stats.report-interval-millis:600000}") private long reportIntervalMillis; - @Scheduled( fixedDelayString = "${edges.stats.report-interval-millis:600000}", initialDelayString = "${edges.stats.report-interval-millis:600000}" diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java index ed98ebe16d..2789b0d7b8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -16,8 +16,6 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.protobuf.AbstractMessage; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; @@ -35,24 +33,16 @@ import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.EntityDataProto; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.edge.stats.EdgeStatsService; import java.time.Duration; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; @@ -64,19 +54,10 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { private static final String STATISTICS_DEVICE_PROFILE = "STATISTICS"; - private final Map EXPECTED_EDGE_STATS = Map.of( - DOWNLINK_MSGS_ADDED, 6L, - DOWNLINK_MSGS_PUSHED, 6L, - DOWNLINK_MSGS_PERMANENTLY_FAILED, 0L, - DOWNLINK_MSGS_TMP_FAILED, 0L - ); - - private final Map EXPECTED_EMPTY_EDGE_STATS = Map.of( - DOWNLINK_MSGS_ADDED, 0L, - DOWNLINK_MSGS_PUSHED, 0L, - DOWNLINK_MSGS_PERMANENTLY_FAILED, 0L, - DOWNLINK_MSGS_TMP_FAILED, 0L - ); + private static final long EXPECTED_MSGS_ADDED = 6L; + private static final long EXPECTED_MSGS_PUSHED = 6L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 0L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; @Autowired private EdgeStatsService edgeStatsService; @@ -85,209 +66,83 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { @Test public void testFullEdgeStatsCycle() throws Exception { - // 1. Clear previous stats and prepare test data + // GIVEN prepareTestData(); - // 2. Wait until Edge counters are updated - awaitEdgeCountersUpdated(); - - // 3. Report statistics - edgeStatsService.reportStats(); - - // 4. Wait until timeseries data is persisted - awaitStatsMatch(EXPECTED_EDGE_STATS); - - // 5. Send statistics to Edge - List latestStatsEntries = tsService.findLatest( - tenantId, - edge.getId(), - Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() - ).get(); - sendStatsToEdge(latestStatsEntries); - - // 6. Wait until telemetry Proto contains the expected stats + // Await Edge Counters Updated await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { - EntityDataProto latestMsg = getLatestEntityDataMessage(); - Map actualStats = toMap(latestMsg.getPostTelemetryMsg().getTsKvList(0)); - assertAllStatsEqual(EXPECTED_EDGE_STATS, actualStats, "Proto stats"); + MsgCounters counters = statsCounterService.getMsgCountersByEdge().get(edge.getId()); + assertEquals(EXPECTED_MSGS_ADDED, counters.getMsgsAdded().get()); + assertEquals(EXPECTED_MSGS_PUSHED, counters.getMsgsPushed().get()); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, counters.getMsgsPermanentlyFailed().get()); + assertEquals(EXPECTED_MSGS_TMP_FAILED, counters.getMsgsTmpFailed().get()); }); - } - @Test - public void testNoMessagesFromEdge() throws ExecutionException, InterruptedException { - // 1. Clear stats counters for the Edge - statsCounterService.clear(edge.getId()); + Thread.sleep(1000); - // 2. Report stats with no data from the Edge + // WHEN edgeStatsService.reportStats(); - // 3. Verify that persisted timeseries contains only empty stats - awaitStatsMatch(EXPECTED_EMPTY_EDGE_STATS); - - List actual = tsService.findLatest( - tenantId, - edge.getId(), - Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() - ).get(); - - assertAllStatsEqual(EXPECTED_EMPTY_EDGE_STATS, toMap(actual), "Empty stats"); - } - - @Test - public void testRepeatedReportStatsDoesNotDuplicate() throws ExecutionException, InterruptedException { - // 1. Clear previous stats and prepare test data - prepareTestData(); - - // 2. Wait until Edge counters are updated - awaitEdgeCountersUpdated(); - - // 3. First report call - edgeStatsService.reportStats(); - - // 4. Verify that the persisted stats match expectations - awaitStatsMatch(EXPECTED_EDGE_STATS); - - // 5. Remove persisted stats to simulate a re-report scenario - tsService.removeLatest( - tenantId, - edge.getId(), - Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() - ); - - // 6. Second report call without increments (counters already cleared) - edgeStatsService.reportStats(); - - // 7. Verify that the stats are empty after the second report - awaitStatsMatch(EXPECTED_EMPTY_EDGE_STATS); - } - - private void awaitStatsMatch(Map expected) { + // THEN await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { - Map actualStats = fetchLatestStats(); - assertAllStatsEqual(expected, actualStats, "Timeseries stats"); + List actualStats = fetchLatestStats(); + assertEquals(EXPECTED_MSGS_ADDED, getStatsLongValue(actualStats, DOWNLINK_MSGS_ADDED)); + assertEquals(EXPECTED_MSGS_PUSHED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PUSHED)); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PERMANENTLY_FAILED)); + assertEquals(EXPECTED_MSGS_TMP_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_TMP_FAILED)); }); } - private Map fetchLatestStats() throws ExecutionException, InterruptedException { - List latestStatsEntries = tsService.findLatest( + private long getStatsLongValue(List stats, EdgeStatsKey key) { + return stats.stream().filter(e -> e.getKey().equals(key.getKey())).findFirst().get().getLongValue().orElse(0L); + } + + private List fetchLatestStats() throws ExecutionException, InterruptedException { + return tsService.findLatest( tenantId, edge.getId(), - Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList() - ).get(); - return toMap(latestStatsEntries); + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList()).get(); } private void prepareTestData() throws InterruptedException, ExecutionException { statsCounterService.clear(edge.getId()); - // 2 stats messages: ADDED Device Profile, ASSIGN Device + + // Save device and assign to edge + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE_PROFILE: ADDED}, {DEVICE: ASSIGNED_TO_EDGE}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{deviceProfileUpdateMsg}, {deviceUpdateMsg, deviceProfileUpdateMsg, deviceCredentialsUpdateMsg}] edgeImitator.expectMessageAmount(4); - Device device = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); - doPost("/api/edge/" + edge.getUuidId() + "/device/" + device.getUuidId(), Device.class); - edgeImitator.waitForMessages(); - // 1 stats message: ASSIGN Asset + Device savedDevice = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); + doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Save asset and assign to edge + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{ASSET: ASSIGNED_TO_EDGE}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{assetUpdateMsg, assetProfileUpdateMsg}] edgeImitator.expectMessageAmount(2); - Asset savedAsset = saveAsset("Edge Asset 2"); + Asset savedAsset = saveAsset("Edge Asset"); doPost("/api/edge/" + edge.getUuidId() + "/asset/" + savedAsset.getUuidId(), Asset.class); Assert.assertTrue(edgeImitator.waitForMessages()); - // 2 stats messages: ADDED Customer, ASSIGN Customer - edgeImitator.expectMessageAmount(1); + // Create customer and assign edge to the customer + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{CUSTOMER: ADDED}, {EDGE: ASSIGNED_TO_CUSTOMER}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{customerUpdateMsg}, {edgeConfiguration}] + edgeImitator.expectMessageAmount(2); Customer customer = new Customer(); customer.setTitle("Edge Customer"); Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - Assert.assertFalse(edgeImitator.waitForMessages(5)); - - // assign edge to customer - edgeImitator.expectMessageAmount(2); doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); - // 1 stats message: Timeseries + // Send device telemetry downlink for the device + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE: TIMESERIES_UPDATED}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{entityData}] edgeImitator.expectMessageAmount(1); String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); - EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, savedDevice.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); edgeEventService.saveAsync(edgeEvent).get(); Assert.assertTrue(edgeImitator.waitForMessages()); } - - private void assertAllStatsEqual(Map expected, Map actual, String context) { - assertAll(context, - expected.entrySet().stream() - .map(e -> () -> assertEquals(e.getValue(), actual.get(e.getKey().getKey()), "Mismatch for stat: " + e.getKey())) - ); - } - - private void awaitEdgeCountersUpdated() { - await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { - MsgCounters counters = statsCounterService.getMsgCountersByEdge().get(edge.getId()); - Map actualCounters = toMap( - Map.entry(DOWNLINK_MSGS_ADDED.getKey(), () -> counters.getMsgsAdded().get()), - Map.entry(DOWNLINK_MSGS_PUSHED.getKey(), () -> counters.getMsgsPushed().get()), - Map.entry(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey(), () -> counters.getMsgsPermanentlyFailed().get()), - Map.entry(DOWNLINK_MSGS_TMP_FAILED.getKey(), () -> counters.getMsgsTmpFailed().get()) - ); - assertAllStatsEqual(EXPECTED_EDGE_STATS, actualCounters, "Edge counters"); - }); - } - - @SafeVarargs - private Map toMap(Map.Entry>... suppliers) { - return Arrays.stream(suppliers) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); - } - - private Map toMap(List stats) { - return stats.stream() - .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(0L))); - } - - private Map toMap(TransportProtos.TsKvListProto kvList) { - Map map = kvList.getKvList().stream() - .collect(Collectors.toMap( - TransportProtos.KeyValueProto::getKey, - TransportProtos.KeyValueProto::getLongV - )); - for (EdgeStatsKey key : EdgeStatsKey.values()) { - map.putIfAbsent(key.getKey(), 0L); - } - return map; - } - - private void sendStatsToEdge(List stats) throws Exception { - edgeImitator.expectMessageAmount(1); - EdgeEvent edgeEvent = constructEdgeEvent( - tenantId, - edge.getId(), - EdgeEventActionType.TIMESERIES_UPDATED, - edge.getId().getId(), - EdgeEventType.EDGE, - buildStatsJson(System.currentTimeMillis(), stats) - ); - edgeEventService.saveAsync(edgeEvent).get(); - assertTrue(edgeImitator.waitForMessages()); - } - - private EntityDataProto getLatestEntityDataMessage() { - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - assertInstanceOf(EntityDataProto.class, latestMessage); - EntityDataProto msg = (EntityDataProto) latestMessage; - assertEquals(edge.getUuidId().getMostSignificantBits(), msg.getEntityIdMSB()); - assertEquals(edge.getUuidId().getLeastSignificantBits(), msg.getEntityIdLSB()); - assertEquals(edge.getId().getEntityType().name(), msg.getEntityType()); - assertTrue(msg.hasPostTelemetryMsg()); - return msg; - } - - private ObjectNode buildStatsJson(long ts, List statsEntries) { - ObjectNode entityBody = JacksonUtil.newObjectNode(); - entityBody.put("ts", ts); - ObjectNode data = JacksonUtil.newObjectNode(); - statsEntries.forEach(entry -> data.put(entry.getKey(), entry.getValueAsString())); - entityBody.set("data", data); - return entityBody; - } - } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index 0b0f28e923..3cb4bec803 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -64,6 +64,13 @@ public class EdgeStatsTest { private static final int TTL_DAYS = 30; private static final long REPORT_INTERVAL_MILLIS = 600_000L; + private static final long EXPECTED_MSGS_ADDED = 5L; + private static final long EXPECTED_MSGS_PUSHED = 3L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 1L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; + private static final long EXPECTED_MSGS_LAG = 10L; + private static final long EXPECTED_MSGS_KAFKA_LAG = 15L; + @Mock private TimeseriesService tsService; @Mock @@ -97,12 +104,38 @@ public class EdgeStatsTest { @Test public void testReportStatsSavesTelemetry() { + // GIVEN + setupCounters(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map counters = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_LAG, counters.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); + } + + @Test + public void testReportStatsWithKafkaLag() { + // GIVEN + setupCounters(); + setupKafkaLag(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map valuesByKey = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_KAFKA_LAG, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); + } + + private void setupCounters() { MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(5); - counters.getMsgsPushed().set(3); - counters.getMsgsPermanentlyFailed().set(1); - counters.getMsgsTmpFailed().set(0); - counters.getMsgsLag().set(10); + counters.getMsgsAdded().set(EXPECTED_MSGS_ADDED); + counters.getMsgsPushed().set(EXPECTED_MSGS_PUSHED); + counters.getMsgsPermanentlyFailed().set(EXPECTED_MSGS_PERMANENTLY_FAILED); + counters.getMsgsTmpFailed().set(EXPECTED_MSGS_TMP_FAILED); + counters.getMsgsLag().set(EXPECTED_MSGS_LAG); ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); countersByEdge.put(edgeId, counters); @@ -111,9 +144,9 @@ public class EdgeStatsTest { when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + } - edgeStatsService.reportStats(); - + private Map verifyCounters() { verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); verify(statsCounterService, times(1)).clear(edgeId); @@ -123,50 +156,23 @@ public class EdgeStatsTest { Map valuesByKey = entries.stream() .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); - Assertions.assertEquals(5L, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); - Assertions.assertEquals(3L, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); - Assertions.assertEquals(1L, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); - Assertions.assertEquals(0L, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); - Assertions.assertEquals(10L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_ADDED, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PUSHED, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_TMP_FAILED, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); + return valuesByKey; } - @Test - public void testReportStatsWithKafkaLag() { - MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(2); - counters.getMsgsPushed().set(2); - counters.getMsgsPermanentlyFailed().set(0); - counters.getMsgsTmpFailed().set(1); - counters.getMsgsLag().set(0); - - ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); - countersByEdge.put(edgeId, counters); - - when(statsCounterService.getMsgCountersByEdge()).thenReturn(countersByEdge); - + private void setupKafkaLag() { String topic = "edge-topic"; TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); when(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId)).thenReturn(partitionInfo); KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) - .thenReturn(Map.of(topic, 15L)); - - when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) - .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + .thenReturn(Map.of(topic, EXPECTED_MSGS_KAFKA_LAG)); edgeStatsService = createEdgeStatsService(Optional.of(kafkaAdmin)); - - edgeStatsService.reportStats(); - - verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); - verify(statsCounterService, times(1)).clear(edgeId); - - List entries = captor.getValue(); - Map valuesByKey = entries.stream() - .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); - - Assertions.assertEquals(15L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java index ecf1376a73..e38b115e88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java @@ -68,7 +68,8 @@ public class PostgresEdgeEventService extends BaseEdgeEventService { Futures.addCallback(saveFuture, new FutureCallback<>() { @Override public void onSuccess(Void result) { - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(edgeEvent.getTenantId()) .entityId(edgeEvent.getEdgeId()) From 916132d6b4cbb0180df446888b75fa0b103208e0 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 10 Dec 2025 10:07:16 +0200 Subject: [PATCH 063/147] Test naming conventions --- .../thingsboard/server/edge/EdgeStatsIntegrationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java index 2789b0d7b8..71722ad2a8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -65,9 +65,9 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { private EdgeStatsCounterService statsCounterService; @Test - public void testFullEdgeStatsCycle() throws Exception { + public void testReportStats() throws Exception { // GIVEN - prepareTestData(); + simulateEdgeEventsAddedDownlinkPushed(); // Await Edge Counters Updated await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { @@ -104,7 +104,7 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList()).get(); } - private void prepareTestData() throws InterruptedException, ExecutionException { + private void simulateEdgeEventsAddedDownlinkPushed() throws InterruptedException, ExecutionException { statsCounterService.clear(edge.getId()); // Save device and assign to edge From 602942fb63ce6fed13260cbf1acf32a33f9e4a20 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 10 Dec 2025 11:03:56 +0200 Subject: [PATCH 064/147] added fetch by ids entities endpoints --- .../app/core/http/asset-profile.service.ts | 5 +++ ui-ngx/src/app/core/http/customer.service.ts | 4 +++ ui-ngx/src/app/core/http/dashboard.service.ts | 5 +++ .../app/core/http/device-profile.service.ts | 5 +++ .../src/app/core/http/entity-view.service.ts | 5 +++ ui-ngx/src/app/core/http/entity.service.ts | 36 +++++-------------- .../src/app/core/http/rule-chain.service.ts | 5 +++ ui-ngx/src/app/core/http/tenant.service.ts | 4 +++ ui-ngx/src/app/core/http/user.service.ts | 4 +++ ui-ngx/src/app/core/http/widget.service.ts | 5 +++ 10 files changed, 51 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/core/http/asset-profile.service.ts b/ui-ngx/src/app/core/http/asset-profile.service.ts index 544779dcd9..8cbad08812 100644 --- a/ui-ngx/src/app/core/http/asset-profile.service.ts +++ b/ui-ngx/src/app/core/http/asset-profile.service.ts @@ -38,6 +38,11 @@ export class AssetProfileService { return this.http.get>(`/api/assetProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getAssetProfilesByIds(assetProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assetProfileInfos?assetProfileIds=${assetProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts index ec8955ca4b..3553b417f0 100644 --- a/ui-ngx/src/app/core/http/customer.service.ts +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -41,6 +41,10 @@ export class CustomerService { return this.http.get(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); } + public getCustomersByIds(customerIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/customers?customerIds=${customerIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public saveCustomer(customer: Customer, config?: RequestConfig): Observable; public saveCustomer(customer: Customer, saveParams: SaveEntityParams, config?: RequestConfig): Observable; public saveCustomer(customer: Customer, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 0a27287e7f..a99bdbbcaf 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -83,6 +83,11 @@ export class DashboardService { return this.http.get(`/api/dashboard/info/${dashboardId}`, defaultHttpOptionsFromConfig(config)); } + public getDashboards(dashboardIds: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/dashboards?dashboardIds=${dashboardIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public saveDashboard(dashboard: Dashboard, config?: RequestConfig): Observable { return this.http.post('/api/dashboard', dashboard, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 91b7314ad2..faf5ccedb3 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -50,6 +50,11 @@ export class DeviceProfileService { return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getDeviceProfilesByIds(deviceProfileIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfileInfos?deviceProfileIds=${deviceProfileIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index 7285c5420f..d951cfee80 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -48,6 +48,11 @@ export class EntityViewService { return this.http.get(`/api/entityView/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } + public getEntityViews(entityViewIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/entityViews?entityViewIds=${entityViewIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getEntityViewInfo(entityViewId: string, config?: RequestConfig): Observable { return this.http.get(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 4daae2115d..d6556c3ced 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -245,50 +245,34 @@ export class EntityService { observable = this.edgeService.getEdges(entityIds, config); break; case EntityType.ENTITY_VIEW: - observable = this.getEntitiesByIdsObservable( - (id) => this.entityViewService.getEntityView(id, config), - entityIds); + observable = this.entityViewService.getEntityViews(entityIds, config); break; case EntityType.TENANT: - observable = this.getEntitiesByIdsObservable( - (id) => this.tenantService.getTenant(id, config), - entityIds); + observable = this.tenantService.getTenantsByIds(entityIds, config); break; case EntityType.CUSTOMER: - observable = this.getEntitiesByIdsObservable( - (id) => this.customerService.getCustomer(id, config), - entityIds); + observable = this.customerService.getCustomersByIds(entityIds, config); break; case EntityType.DASHBOARD: - observable = this.getEntitiesByIdsObservable( - (id) => this.dashboardService.getDashboardInfo(id, config), - entityIds); + observable = this.dashboardService.getDashboards(entityIds, config); break; case EntityType.USER: - observable = this.getEntitiesByIdsObservable( - (id) => this.userService.getUser(id, config), - entityIds); + observable = this.userService.getUsersByIds(entityIds, config); break; case EntityType.ALARM: console.error('Get Alarm Entity is not implemented!'); break; case EntityType.DEVICE_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.deviceProfileService.getDeviceProfileInfo(id, config), - entityIds); + observable = this.deviceProfileService.getDeviceProfilesByIds(entityIds, config); break; case EntityType.TENANT_PROFILE: observable = this.tenantProfileService.getTenantProfilesByIds(entityIds, config); break; case EntityType.ASSET_PROFILE: - observable = this.getEntitiesByIdsObservable( - (id) => this.assetProfileService.getAssetProfileInfo(id, config), - entityIds); + observable = this.assetProfileService.getAssetProfilesByIds(entityIds, config); break; case EntityType.WIDGETS_BUNDLE: - observable = this.getEntitiesByIdsObservable( - (id) => this.widgetService.getWidgetsBundle(id, config), - entityIds); + observable = this.widgetService.getWidgetsBundlesByIds(entityIds, config); break; case EntityType.NOTIFICATION_TARGET: observable = this.notificationService.getNotificationTargetsByIds(entityIds, config); @@ -300,9 +284,7 @@ export class EntityService { observable = this.oauth2Service.findTenantOAuth2ClientInfosByIds(entityIds, config); break; case EntityType.RULE_CHAIN: - observable = this.getEntitiesByIdsObservable( - (id) => this.ruleChainService.getRuleChain(id, config), - entityIds); + observable = this.ruleChainService.getRuleChainsByIds(entityIds, config); break; case EntityType.TB_RESOURCE: observable = this.resourceService.getResourcesByIds(entityIds, config); diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index ba80632c40..197a1876ff 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -68,6 +68,11 @@ export class RuleChainService { defaultHttpOptionsFromConfig(config)); } + public getRuleChainsByIds(ruleChainIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains?&ruleChainIds=${ruleChainIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + public getRuleChain(ruleChainId: string, config?: RequestConfig): Observable { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/tenant.service.ts b/ui-ngx/src/app/core/http/tenant.service.ts index f8c62c6ae3..8ac4fc68eb 100644 --- a/ui-ngx/src/app/core/http/tenant.service.ts +++ b/ui-ngx/src/app/core/http/tenant.service.ts @@ -35,6 +35,10 @@ export class TenantService { return this.http.get>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } + public getTenantsByIds(tenantIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenants?tenantIds=${tenantIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public getTenantInfos(pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenantInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 4d96eca8cd..b69e0d49b2 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -61,6 +61,10 @@ export class UserService { return this.http.get(`/api/user/${userId}`, defaultHttpOptionsFromConfig(config)); } + public getUsersByIds(userIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/users?userIds=${userIds.join(',')}`, defaultHttpOptionsFromConfig(config)); + } + public saveUser(user: User, sendActivationMail: boolean = false, config?: RequestConfig): Observable { let url = '/api/user'; diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 00a90f632a..ae09cc4ba3 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -331,6 +331,11 @@ export class WidgetService { this.widgetsInfoInMemoryCache.delete(fullFqn); } + public getWidgetsBundlesByIds(widgetsBundleIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetsBundles?widgetsBundleIds=${widgetsBundleIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + private loadWidgetsBundleCache(config?: RequestConfig): Observable { if (!this.allWidgetsBundles) { if (!this.loadWidgetsBundleCacheSubject) { From 6f1dcba0163e684c9ef81ab5d57c44bae853ed7a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Dec 2025 15:05:03 +0200 Subject: [PATCH 065/147] Update entity limit management link for entities limit request notification. --- .../thingsboard/server/controller/NotificationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 1508354f19..60785b79a7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -312,7 +312,7 @@ public class NotificationController extends BaseController { .entityType(entityType) .userEmail(user.getEmail()) .increaseLimitActionLabel("Set new limit") - .increaseLimitLink("/tenantProfiles/"+tenantService.findTenantById(user.getTenantId()).getTenantProfileId().toString()) + .increaseLimitLink("/tenants/"+user.getTenantId().toString()) .baseUrl(baseUrl) .build(); notificationCenter.sendSystemNotification(TenantId.SYS_TENANT_ID, notificationTargetId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, info); From 8ae4fe3fe43ee987fc2c2076434c6a467948ef3c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 10 Dec 2025 15:13:40 +0200 Subject: [PATCH 066/147] Update entity limit management link for entities limit request notification. --- .../dialog/entity-limit-exceeded-dialog.component.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts index 7efabb0fcb..ce6491071e 100644 --- a/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts @@ -24,7 +24,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@core/services/dialog.service'; import { AuthService } from '@core/auth/auth.service'; -import { TenantService } from '@core/http/tenant.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { NotificationService } from '@core/http/notification.service'; @@ -51,7 +50,6 @@ export class EntityLimitExceededDialogComponent extends DialogComponent { - this.authService.redirectUrl = `/tenantProfiles/${tenant.tenantProfileId.id}`; - this.authService.logout(); - } - ); + this.authService.redirectUrl = `/tenants/${getCurrentAuthUser(this.store).tenantId}`; + this.authService.logout(); } } From cadafa1591112f1d08cee25e6d0b42675d82fed1 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 10 Dec 2025 16:07:31 +0200 Subject: [PATCH 067/147] UI: Fixed color settings panel placement --- .../widget/lib/settings/common/color-settings.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 05c069cbf3..1e41cea52d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -164,7 +164,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD renderer: this.renderer, componentType: ColorSettingsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'left', + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], context: { colorSettings: this.modelValue, settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), From f51e13f9e04a8c1baff9527ca90d7505c63648a3 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 10 Dec 2025 15:24:17 +0200 Subject: [PATCH 068/147] Added the ability to send only updated arguments for propagation CF --- .../cf/ctx/state/CalculatedFieldState.java | 2 +- .../PropagationCalculatedFieldState.java | 44 +++++++- .../cf/CalculatedFieldIntegrationTest.java | 57 ++++++---- .../PropagationCalculatedFieldStateTest.java | 101 ++++++++++++++---- 4 files changed, 160 insertions(+), 44 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index f254631491..542759df49 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -110,7 +110,7 @@ public interface CalculatedFieldState extends Closeable { private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " + "Verify the configured relation type and direction."; private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: "; - private static final ReadinessStatus READY = new ReadinessStatus(true, null); + public static final ReadinessStatus READY = new ReadinessStatus(true, null); public static ReadinessStatus from(List emptyOrMissingArguments) { if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 5a7753c86a..c55a227ea0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; @@ -68,8 +69,9 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry)) { return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); } + boolean newEntityAdded = propagationArgumentEntry.getAdded() != null; List entityIds; - if (propagationArgumentEntry.getAdded() != null) { + if (newEntityAdded) { entityIds = List.of(propagationArgumentEntry.getAdded()); propagationArgumentEntry.setAdded(null); } else { @@ -86,13 +88,43 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState .build(), MoreExecutors.directExecutor()); } + if (newEntityAdded || CollectionsUtil.isEmpty(updatedArgs)) { + updatedArgs = arguments; + } return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() .entityIds(entityIds) - .result(toTelemetryResult(ctx)) + .result(toTelemetryResult(ctx, updatedArgs)) .build()); } - private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) { + @Override + protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + if (ctx.isApplyExpressionForResolvedArguments() || currentArguments == null) { + return super.checkReadiness(requiredArguments, currentArguments); + } + boolean propagationNotEmpty = false; + boolean hasOtherNonEmpty = false; + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } else if (PROPAGATION_CONFIG_ARGUMENT.equals(requiredArgumentKey)) { + propagationNotEmpty = true; + } else { + hasOtherNonEmpty = true; + } + } + if (propagationNotEmpty && hasOtherNonEmpty) { + return ReadinessStatus.READY; + } + return ReadinessStatus.from(emptyArguments); + } + + private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx, Map updatedArgs) { Output output = ctx.getOutput(); TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = TelemetryCalculatedFieldResult.builder() @@ -100,12 +132,14 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState .type(output.getType()) .scope(output.getScope()); ObjectNode valuesNode = JacksonUtil.newObjectNode(); - arguments.forEach((outputKey, argumentEntry) -> { + updatedArgs.forEach((outputKey, argumentEntry) -> { if (argumentEntry instanceof PropagationArgumentEntry) { return; } if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { - JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + if (!singleArgumentEntry.isEmpty()) { + JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + } return; } throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index f0008d6cfb..a194680ca1 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -1089,7 +1089,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Telemetry on device doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk()); + JacksonUtil.toJsonNode("{\"temperature\":12.5, \"humidity\":85}")).andExpect(status().isOk()); // --- Build CF: PROPAGATION with expression --- CalculatedField cf = new CalculatedField(); @@ -1102,11 +1102,14 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); cfg.setApplyExpressionToResolvedArguments(true); - Argument arg = new Argument(); - arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setArguments(Map.of("t", arg)); + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setExpression("{\"testResult\": t * 2}"); + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("t", arg1, "h", arg2)); + cfg.setExpression("return { testResult: (t + h) / 2};"); AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); @@ -1125,8 +1128,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); assertThat(attrs1).isNotNull(); assertThat(attrs2).isNotNull(); - assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0); - assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0); + assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(48.75); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(48.75); }); String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", @@ -1148,7 +1151,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); assertThat(attrs1).isNullOrEmpty(); assertThat(attrs2).isNotNull(); - assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(55); }); } @@ -1167,7 +1170,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Telemetry on device long ts = System.currentTimeMillis() - 300000L; - postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts)); + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5, \"humidity\":85}}", ts)); // --- Build CF: PROPAGATION without expression --- CalculatedField cf = new CalculatedField(); @@ -1180,9 +1183,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode - Argument arg = new Argument(); - arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setArguments(Map.of("temperatureComputed", arg)); + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("temperatureComputed", arg1, "humidityComputed", arg2)); TimeSeriesOutput output = new TimeSeriesOutput(); output.setStrategy(new TimeSeriesImmediateOutputStrategy(0, true, true, true, true)); @@ -1197,14 +1203,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); - ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry1).isNotNull(); assertThat(telemetry2).isNotNull(); assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry1.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", @@ -1212,10 +1222,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE ); doDelete(deleteUrl).andExpect(status().isOk()); - doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed,humidityComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); - // Update telemetry on device - long newTs = System.currentTimeMillis() - 300000L; + // Update telemetry on the device + long newTs = ts + 300000L; postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs)); // --- Assert propagated calculation (arguments-only mode after update) --- @@ -1223,13 +1233,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); - ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry1).isNotNull(); assertThat(telemetry2).isNotNull(); assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry1.get("humidityComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + // TS for humidity is not updated -> expected + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); Asset asset3 = createAsset("Propagated Asset 3", null); @@ -1241,10 +1256,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed"); + ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry).isNotNull(); assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + assertThat(telemetry.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index add6c1ee39..346dbbcb7c 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -70,17 +70,26 @@ import static org.thingsboard.server.common.data.cf.configuration.PropagationCal public class PropagationCalculatedFieldStateTest { private static final String TEMPERATURE_ARGUMENT_NAME = "t"; + private static final String HUMIDITY_ARGUMENT_NAME = "h"; private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; private static final double TEMPERATURE_VALUE = 12.5; + private static final double HUMIDITY_VALUE = 85; + + private static final PropagationArgumentEntry EMPTY_PROPAGATION_ARGUMENT = new PropagationArgumentEntry(Collections.emptyList()); private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); - private final SingleValueArgumentEntry singleValueArgEntry = + private final SingleValueArgumentEntry EMPTY_SINGLE_VALUE_ARGUMENT = new SingleValueArgumentEntry(); + + private final SingleValueArgumentEntry temperatureArgumentEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); + private final SingleValueArgumentEntry humidityArgumentEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("humidity", HUMIDITY_VALUE), 99L); + private final PropagationArgumentEntry propagationArgEntry = new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); @@ -121,7 +130,7 @@ public class PropagationCalculatedFieldStateTest { @Test void testInitAddsRequiredArgument() { initCtxAndState(false); - assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, HUMIDITY_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -133,7 +142,7 @@ public class PropagationCalculatedFieldStateTest { private static Stream provideInvalidPropagationArgs() { return Stream.of( null, - new PropagationArgumentEntry(Collections.emptyList()) + EMPTY_PROPAGATION_ARGUMENT ); } @@ -143,7 +152,8 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); Map args = new HashMap<>(); - args.put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); // Valid user arg + args.put(TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry); // Valid user arg + args.put(HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry); // Valid user arg if (propagationEntry != null) { args.put(PROPAGATION_CONFIG_ARGUMENT, propagationEntry); @@ -155,19 +165,54 @@ public class PropagationCalculatedFieldStateTest { } @Test - void testIsReadyWhenPropagationArgHasEntities() { + void testIsReadyWithoutExpressionWhenAllArgumentsAreNotEmpty() { initCtxAndState(false); - state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(updatedArgs, ctx); assertThat(state.isReady()).isTrue(); assertThat(state.getReadinessStatus().errorMsg()).isNull(); } + @Test + void testIsReadyWithoutExpressionWhenAtLeastOneArgumentIsNotEmpty() { + initCtxAndState(false); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + @Test + void testIsNotReadyWithExpressionWhenAtLeastOneArgumentIsEmpty() { + initCtxAndState(true); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).isEqualTo("Required arguments are missing: h"); + } + @Test void testPerformCalculationWithEmptyPropagationArg() throws Exception { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, EMPTY_PROPAGATION_ARGUMENT); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + // test empty propagation argument calculation PropagationCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); @@ -178,8 +223,12 @@ public class PropagationCalculatedFieldStateTest { @Test void testPerformCalculationWithArgumentsOnlyMode() throws Exception { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); PropagationCalculatedFieldResult propagationResult = performCalculation(); @@ -193,7 +242,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); ObjectNode expectedNode = JacksonUtil.newObjectNode(); - JacksonUtil.addKvEntry(expectedNode, singleValueArgEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); + JacksonUtil.addKvEntry(expectedNode, temperatureArgumentEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); assertThat(result.getResult()).isEqualTo(expectedNode); } @@ -201,9 +250,13 @@ public class PropagationCalculatedFieldStateTest { @Test void testPerformCalculationWithExpressionResultMode() throws Exception { initCtxAndState(true); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); PropagationCalculatedFieldResult propagationResult = performCalculation(); assertThat(propagationResult).isNotNull(); @@ -216,7 +269,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); ObjectNode expectedNode = JacksonUtil.newObjectNode(); - expectedNode.put(TEST_RESULT_EXPRESSION_KEY, TEMPERATURE_VALUE * 2); + expectedNode.put(TEST_RESULT_EXPRESSION_KEY, (TEMPERATURE_VALUE + HUMIDITY_VALUE) / 2); assertThat(result.getResult()).isEqualTo(expectedNode); } @@ -224,8 +277,13 @@ public class PropagationCalculatedFieldStateTest { @Test void testPropagationWithUpdatedPropagationArgument() throws ExecutionException, InterruptedException { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); AssetId newEntityId = new AssetId(UUID.fromString("83e2c962-eeae-4708-984e-e6a24760f9c3")); PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(); @@ -236,6 +294,9 @@ public class PropagationCalculatedFieldStateTest { PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(updated); assertThat(propagationCalculatedFieldResult).isNotNull(); assertThat(propagationCalculatedFieldResult.getEntityIds()).isNotNull().containsExactly(newEntityId); + assertThat(propagationCalculatedFieldResult.getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isEqualTo(JacksonUtil.newObjectNode().put(TEMPERATURE_ARGUMENT_NAME, TEMPERATURE_VALUE)); } private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { @@ -260,8 +321,12 @@ public class PropagationCalculatedFieldStateTest { ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); temperatureArg.setRefEntityKey(tempKey); - config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); - config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); + Argument humidityArg = new Argument(); + ReferencedEntityKey humidityKey = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null); + humidityArg.setRefEntityKey(humidityKey); + + config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg, HUMIDITY_ARGUMENT_NAME, humidityArg)); + config.setExpression("return { " + TEST_RESULT_EXPRESSION_KEY + ": (" + TEMPERATURE_ARGUMENT_NAME + " + " + HUMIDITY_ARGUMENT_NAME + ") / 2};"); AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); From 39a5bd256125d84d1776573d934aeda68b228bca Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 10 Dec 2025 17:43:32 +0200 Subject: [PATCH 069/147] UI updates --- .../propagation-configuration.component.html | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index bd92d25f92..570beb83fa 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -77,7 +77,7 @@
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 54492e06b5..e078826853 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1121,7 +1121,6 @@ "datasource": "Datasource", "add-argument": "Add argument", "test-script-function": "Test script function", - "test-expression-function": "Test expression function", "no-arguments": "At least one argument is required.", "argument-settings": "Argument settings", "argument-current": "Current entity", @@ -1311,8 +1310,8 @@ "offset-value-required": "Offset value is required", "offset-value-min": "Offset value must be a positive integer", "offset-value-max": "Offset value should be less than the aggregate interval value", - "wait-delay": "Wait for delayed telemetry", - "wait-delay-hint": "Waits for delayed telemetry after the interval ends", + "wait-delay": "Apply await timeout for delayed telemetry", + "wait-delay-hint": "Defines how long to wait for delayed telemetry after the interval ends. If such telemetry arrives, the result for that interval will be recalculated.\n", "duration": "Duration", "duration-required": "Duration is required", "duration-min": "Duration should be at least 1 minute", From c68ea4d177f5e0a63ccf97b36e32711bd3747c6c Mon Sep 17 00:00:00 2001 From: Anatolii Davydko Date: Wed, 10 Dec 2025 16:11:56 +0000 Subject: [PATCH 070/147] add separate folder for ResourceSubType.EXTENSION and for ResourceSubType.MODULE --- .../gateway-management-extension.js | 0 .../org/thingsboard/server/service/install/InstallScripts.java | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename application/src/main/data/resources/{js_modules => js_extensions}/gateway-management-extension.js (100%) diff --git a/application/src/main/data/resources/js_modules/gateway-management-extension.js b/application/src/main/data/resources/js_extensions/gateway-management-extension.js similarity index 100% rename from application/src/main/data/resources/js_modules/gateway-management-extension.js rename to application/src/main/data/resources/js_extensions/gateway-management-extension.js diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f9b5c282a8..cb357e2a59 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -406,7 +406,8 @@ public class InstallScripts { Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR); loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE, null); - loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_extensions"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.MODULE); loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD, null); } From 2f9d142cc9a705a88356bfdbc4436731b34e7f90 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 10 Dec 2025 18:21:59 +0200 Subject: [PATCH 071/147] UI: Redesign login pages --- .../login/create-password.component.html | 86 ++++++------- .../login/create-password.component.scss | 1 + .../pages/login/create-password.component.ts | 14 ++- ...force-two-factor-auth-login.component.html | 22 ++-- ...force-two-factor-auth-login.component.scss | 1 + .../pages/login/link-expired.component.html | 7 +- .../pages/login/link-expired.component.scss | 1 + .../pages/login/link-expired.component.ts | 16 +-- .../login/pages/login/login.component.html | 114 +++++++++--------- .../login/pages/login/login.component.scss | 78 ++++++------ .../login/pages/login/login.component.ts | 25 ++-- .../reset-password-request.component.html | 48 ++++---- .../reset-password-request.component.scss | 1 + .../login/reset-password-request.component.ts | 5 +- .../pages/login/reset-password.component.html | 90 +++++++------- .../pages/login/reset-password.component.scss | 6 +- .../pages/login/reset-password.component.ts | 17 ++- .../two-factor-auth-login.component.html | 4 +- .../two-factor-auth-login.component.scss | 1 + .../assets/locale/locale.constant-ar_AE.json | 1 - .../assets/locale/locale.constant-ca_ES.json | 1 - .../assets/locale/locale.constant-cs_CZ.json | 1 - .../assets/locale/locale.constant-de_DE.json | 1 - .../assets/locale/locale.constant-el_GR.json | 1 - .../assets/locale/locale.constant-en_US.json | 15 +-- .../assets/locale/locale.constant-es_ES.json | 1 - .../assets/locale/locale.constant-fa_IR.json | 1 - .../assets/locale/locale.constant-fr_FR.json | 1 - .../assets/locale/locale.constant-it_IT.json | 1 - .../assets/locale/locale.constant-ja_JP.json | 1 - .../assets/locale/locale.constant-ka_GE.json | 1 - .../assets/locale/locale.constant-ko_KR.json | 1 - .../assets/locale/locale.constant-lt_LT.json | 1 - .../assets/locale/locale.constant-lv_LV.json | 1 - .../assets/locale/locale.constant-nl_BE.json | 1 - .../assets/locale/locale.constant-nl_NL.json | 1 - .../assets/locale/locale.constant-no_NO.json | 1 - .../assets/locale/locale.constant-pl_PL.json | 1 - .../assets/locale/locale.constant-pt_BR.json | 1 - .../assets/locale/locale.constant-ro_RO.json | 1 - .../assets/locale/locale.constant-sl_SI.json | 1 - .../assets/locale/locale.constant-tr_TR.json | 1 - .../assets/locale/locale.constant-uk_UA.json | 1 - .../assets/locale/locale.constant-zh_CN.json | 1 - .../assets/locale/locale.constant-zh_TW.json | 1 - ui-ngx/src/styles.scss | 33 +++++ 46 files changed, 288 insertions(+), 322 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index e3e43b6d29..04d7b3ad03 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -17,55 +17,49 @@ -->
- - - login.create-password - + + login.create-password - +
+ - - + +
-
-
- - - common.password - - lock - - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - - login.password-again - - lock - - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - - -
- - -
-
-
+ + common.password + + lock + + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + + + login.password-again + + lock + + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss index 6d5d687d13..ccd82a5060 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-create-password-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-create-password-card { @media #{$mat-gt-xs} { width: 450px !important; diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 85396338d5..9e02ad4eb6 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -16,7 +16,6 @@ import { Component } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -27,17 +26,18 @@ import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/mode templateUrl: './create-password.component.html', styleUrls: ['./create-password.component.scss'] }) -export class CreatePasswordComponent extends PageComponent { +export class CreatePasswordComponent { passwordPolicy: UserPasswordPolicy; createPassword: FormGroup; + isLoading = false; + private activateToken: string; constructor(private route: ActivatedRoute, private authService: AuthService, private fb: FormBuilder) { - super(); this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -60,9 +60,11 @@ export class CreatePasswordComponent extends PageComponent { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { - this.authService.activate( - this.activateToken, - this.createPassword.get('newPassword').value, true).subscribe(); + this.isLoading = true + this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true) + .subscribe({ + error: () => {this.isLoading = false;} + }); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 24f9ec1bd7..d5ee7fd9b1 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -41,7 +41,7 @@ } @if (config) { - } @@ -66,7 +66,7 @@

login.scan-qr-code

login.enter-key-manually

-
+
{{ totpAuthURLSecret }}
-
+
@@ -126,7 +126,7 @@
-
+
@@ -162,7 +162,7 @@

login.email-description

- + @@ -174,7 +174,7 @@ -
+
@@ -223,7 +223,7 @@

login.backup-code-warn

-
@@ -259,7 +259,7 @@ }

- + {{ 'login.verification-code-many-request' | translate }} } -
+
@@ -300,8 +300,8 @@

{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).description | translate }}

-
- @if (isAnyProviderAvailable) { diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss index f1ea95d639..35b642b11f 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss @@ -23,6 +23,7 @@ .tb-two-factor-auth-login-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .progress-bar { z-index: 10; diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html index e9dcd74b9c..d6d6f0c7dd 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.html @@ -23,16 +23,11 @@ {{ title | translate }} - - -
{{ message | translate }}
- diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss index c2c4e270d0..abc1bb51d2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-expired-link-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-expired-link-card { letter-spacing: 0.15px; line-height: 24px; diff --git a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts index 79f0ee5b43..c520cb30cd 100644 --- a/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/link-expired.component.ts @@ -15,33 +15,23 @@ /// import { Component } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'tb-link-expired', templateUrl: './link-expired.component.html', styleUrls: ['./link-expired.component.scss'] }) -export class LinkExpiredComponent extends PageComponent { +export class LinkExpiredComponent { isPasswordLinkExpired: boolean; title: string; message: string; - constructor(protected store: Store, - private route: ActivatedRoute, - private router: Router) { - super(store); + constructor(private route: ActivatedRoute) { this.isPasswordLinkExpired = this.route.snapshot.data.passwordLinkExpired; this.title = this.isPasswordLinkExpired ? 'login.reset-password-link-expired' : 'login.activation-link-expired'; this.message = this.isPasswordLinkExpired ? 'login.reset-password-link-expired-message' : 'login.activation-link-expired-message'; } - - navigateToLoginPage() { - this.router.navigateByUrl('login'); - } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 94350c80f0..0d29df31d1 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -17,71 +17,67 @@ --> diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index 8859aed7e4..331bf70822 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -23,48 +23,49 @@ margin-top: 36px; margin-bottom: 76px; background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-login-form { @media #{$mat-gt-xs} { - width: 550px !important; + width: 480px !important; } .forgot-password { - padding: 0 0.5em 1em; - .tb-reset-password { - padding: 0 6px; - } + --mat-text-button-horizontal-padding: 0; + --mdc-text-button-container-height: 20px; } .tb-action-button{ - padding: 20px 0 16px; + margin: 20px 0 16px; } } - .oauth-container{ - padding: 0; - - .container-divider { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - width: 100%; + .container-divider { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + margin-bottom: 16px; - .line { - flex: 1; - } + .mat-divider-horizontal{ + --mat-divider-color: var(--mdc-outlined-text-field-outline-color); + } - .mat-divider-horizontal{ - position: relative; - } + .text { + padding-right: 8px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + } + } - .text { - padding-right: 8px; - padding-left: 8px; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.15px; - } + .oauth-container{ + .title { + font-size: 20px; + line-height: 24px; + letter-spacing: .1px; + text-align: center; + margin-bottom: 4px; } .material-icons{ @@ -74,33 +75,32 @@ a.login-with-button { color: rgba(black, 0.87); - background-color: map-get($tb-dark-theme-background, raised-button); + background-color: #ffffff; } .login-button-container { a.login-with-button { - --mdc-outlined-button-container-shape: 8px; - max-width: 123px; + max-width: 170px; min-width: 60px; flex-grow: 1; - flex-basis: 120px; + flex-basis: 130px; .tb-mat-20 { - margin: 0; - vertical-align: text-top; + margin-right: 12px; } } - &:has(> :nth-child(2):last-child) { + &:has(> :nth-child(1):last-child) { a.login-with-button { - max-width: 180px; - flex-basis: 180px; + max-width: 100%; } } - &:has(> :nth-child(3):last-child) { + &:has(> :nth-child(2):last-child), + &:has(> :nth-child(4):last-child) { a.login-with-button { - max-width: 180px; + max-width: 100%; + flex-basis: 180px; } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index a4bd2853b9..2046bcb3ed 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -16,9 +16,6 @@ import { Component, OnInit } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; import { UntypedFormBuilder, Validators } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; @@ -31,9 +28,10 @@ import { validateEmail } from '@app/core/utils'; templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) -export class LoginComponent extends PageComponent implements OnInit { +export class LoginComponent implements OnInit { passwordViolation = false; + isLoading = false; loginFormGroup = this.fb.group({ username: ['', [Validators.required, validateEmail]], @@ -41,11 +39,9 @@ export class LoginComponent extends PageComponent implements OnInit { }); oauth2Clients: Array = null; - constructor(protected store: Store, - private authService: AuthService, + constructor(private authService: AuthService, public fb: UntypedFormBuilder, private router: Router) { - super(store); } ngOnInit() { @@ -54,9 +50,11 @@ export class LoginComponent extends PageComponent implements OnInit { login(): void { if (this.loginFormGroup.valid) { - this.authService.login(this.loginFormGroup.value).subscribe( - () => {}, - (error: HttpErrorResponse) => { + this.isLoading = true; + this.authService.login(this.loginFormGroup.value).subscribe({ + next: () => {}, + error: (error: HttpErrorResponse) => { + this.isLoading = false; if (error && error.error && error.error.errorCode) { if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) { this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`); @@ -65,12 +63,9 @@ export class LoginComponent extends PageComponent implements OnInit { } } } - ); - } else { - Object.keys(this.loginFormGroup.controls).forEach(field => { - const control = this.loginFormGroup.get(field); - control.markAsTouched({onlySelf: true}); }); + } else { + this.loginFormGroup.markAllAsTouched(); } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html index abf2e6c61c..df286768ef 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -18,38 +18,32 @@
- - - login.request-password-reset - + + login.request-password-reset +
- +
-
-
- - - login.email - - email - - {{ 'user.invalid-email-format' | translate }} - - -
- - -
-
-
+ + login.email + + email + + {{ 'user.invalid-email-format' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss index 3d5a53ea16..77f76b648f 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.scss @@ -20,6 +20,7 @@ flex: 1 1 0; .tb-request-password-reset-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-request-password-reset-card { @media #{$mat-gt-xs} { width: 450px !important; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts index 3daeaa693d..c7d22e3274 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -29,7 +29,7 @@ import { validateEmail } from '@app/core/utils'; templateUrl: './reset-password-request.component.html', styleUrls: ['./reset-password-request.component.scss'] }) -export class ResetPasswordRequestComponent extends PageComponent implements OnInit { +export class ResetPasswordRequestComponent extends PageComponent { clicked: boolean = false; @@ -44,9 +44,6 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn super(store); } - ngOnInit() { - } - disableInputs() { this.requestPasswordRequest.disable(); this.clicked = true; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index 7492ab654f..a5fe779ab3 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -17,58 +17,52 @@ -->
- - - login.password-reset - - -
{{ 'login.expired-password-reset-message' | translate }}
+ + login.password-reset + + login.expired-password-reset-message - +
+ - - + +
-
-
- - - login.new-password - - lock - - - {{ 'security.password-requirement.password-not-meet-requirements' | translate }} - - - - login.new-password-again - - lock - - - {{ 'security.password-requirement.new-passwords-not-match' | translate }} - - -
- - -
-
-
+ + login.new-password + + lock + + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + + + + login.new-password-again + + lock + + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss index cda127d901..2a3022352e 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.scss @@ -20,15 +20,11 @@ flex: 1 1 0; .tb-reset-password-content { background-color: #eee; + --mdc-elevated-card-container-elevation: none; .tb-reset-password-card { @media #{$mat-gt-sm} { width: 450px !important; } } - - .tb-card-title{ - padding-top: 0; - padding-bottom: 0; - } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 396c0f740d..d062ff0037 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -16,7 +16,6 @@ import { Component } from '@angular/core'; import { AuthService } from '@core/auth/auth.service'; -import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -27,9 +26,10 @@ import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/mode templateUrl: './reset-password.component.html', styleUrls: ['./reset-password.component.scss'] }) -export class ResetPasswordComponent extends PageComponent { +export class ResetPasswordComponent { isExpiredPassword: boolean; + isLoading = false; resetPassword: FormGroup; passwordPolicy: UserPasswordPolicy; @@ -40,7 +40,6 @@ export class ResetPasswordComponent extends PageComponent { private router: Router, private authService: AuthService, private fb: FormBuilder) { - super(); this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -62,13 +61,13 @@ export class ResetPasswordComponent extends PageComponent { onResetPassword() { if (this.resetPassword.invalid) { - this.resetPassword.markAllAsTouched(); + this.resetPassword.markAllAsTouched(); } else { - this.authService.resetPassword( - this.resetToken, - this.resetPassword.get('newPassword').value).subscribe( - () => this.router.navigateByUrl('login') - ); + this.isLoading = true; + this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).subscribe({ + next: () => this.router.navigateByUrl('login'), + error: () => {this.isLoading = false;} + }); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html index 5175c01546..d85cbb0f8b 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html @@ -41,7 +41,7 @@

{{ providerDescription }}

- +
- + + + + + +
+ + + + +
+ +
+ +
+
+
calculated-fields.calculated-field-names
+ + +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
alarm-rule.target-entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + + {{ entityTypeTranslations.get(type)?.type | translate }} + + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
alarm-rule.target-entities
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss new file mode 100644 index 0000000000..fc4402c28f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../scss/constants'; + +:host { + display: flex; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-calculated-fields-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + + tb-entity-subtype-list { + flex: 1; + @media #{$mat-gt-xs} { + width: 180px; + } + .mdc-evolution-chip-set__chips { + width: 100%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts new file mode 100644 index 0000000000..9aa13e7673 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts @@ -0,0 +1,288 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + DestroyRef, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { fromEvent, Subscription } from 'rxjs'; +import { POSITION_MAP } from '@shared/models/overlay.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + calculatedFieldsEntityTypeList, + CalculatedFieldsQuery, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { TranslateService } from '@ngx-translate/core'; + +export const CALCULATED_FIELDS_CONFIG_DATA = new InjectionToken('CalculatedFieldsFilterConfigData'); + +export interface CalculatedFieldsFilterConfigData { + panelMode: boolean; + userMode: boolean; + filterConfig: CalculatedFieldsQuery; + initialFilterConfig?: CalculatedFieldsQuery; +} + +@Component({ + selector: 'tb-calculated-fields-filter-config', + templateUrl: './calculated-fields-filter-config.component.html', + styleUrls: ['./calculated-fields-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldsFilterConfigComponent), + multi: true + } + ] +}) +export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('calculatedFieldsFilterPanel') + calculatedFieldsFilterPanel: TemplateRef; + + @Input() + disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @coerceBoolean() + @Input() + userMode = false; + + @coerceBoolean() + @Input() + propagatedFilter = true; + + @Input() + initialCfFilterConfig: CalculatedFieldsQuery = { + name: [], + types: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + cfFilterForm: FormGroup; + + panelResult: CalculatedFieldsQuery = null; + + entityType = EntityType; + + listEntityTypes = calculatedFieldsEntityTypeList; + entityTypeTranslations = entityTypeTranslations; + + readonly types: StringItemsOption[] = calculatedFieldTypes.map(item => ({ + name: this.translate.instant(CalculatedFieldTypeTranslations.get(item).name), + value: item + })); + + private cfFilterOverlayRef: OverlayRef; + private cfFilterConfig: CalculatedFieldsQuery; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(CALCULATED_FIELDS_CONFIG_DATA) + private data: CalculatedFieldsFilterConfigData | undefined, + @Optional() private overlayRef: OverlayRef, + private fb: FormBuilder, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private translate: TranslateService) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.userMode = this.data.userMode; + this.cfFilterConfig = this.data.filterConfig; + this.initialCfFilterConfig = this.data.initialFilterConfig; + if (this.panelMode && !this.initialCfFilterConfig) { + this.initialCfFilterConfig = deepClone(this.cfFilterConfig); + } + } + this.cfFilterForm = this.fb.group({ + name: [null, []], + types: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.cfFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.cfConfigUpdated(this.cfFilterForm.value); + } + } + ); + if (this.panelMode) { + this.updateCfConfigForm(this.cfFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.cfFilterForm.disable({emitEvent: false}); + } else { + this.cfFilterForm.enable({emitEvent: false}); + } + } + + writeValue(cfFilterConfig?: CalculatedFieldsQuery): void { + this.cfFilterConfig = cfFilterConfig; + if (!this.initialCfFilterConfig && cfFilterConfig) { + this.initialCfFilterConfig = deepClone(cfFilterConfig); + } + this.updateCfConfigForm(cfFilterConfig); + } + + toggleCfFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.cfFilterOverlayRef = this.overlay.create(config); + this.cfFilterOverlayRef.backdropClick().subscribe(() => { + this.cfFilterOverlayRef.dispose(); + }); + this.cfFilterOverlayRef.attach(new TemplatePortal(this.calculatedFieldsFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.cfFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateCfConfigForm(this.cfFilterConfig); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + update() { + this.cfConfigUpdated(this.cfFilterForm.value); + this.cfFilterForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.cfFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + reset() { + const cfFilterConfig = this.cfFilterFromFormValue(this.cfFilterForm.value); + if (!this.cfFilterConfigEquals(cfFilterConfig, this.initialCfFilterConfig)) { + this.updateCfConfigForm(this.initialCfFilterConfig); + this.cfFilterForm.markAsDirty(); + } + } + + private cfFilterConfigEquals = (filter1?: CalculatedFieldsQuery, filter2?: CalculatedFieldsQuery): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.name, filter2.name)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.types, filter2.types)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType !== filter2.entityType; + } + return false; + }; + + private updateCfConfigForm(cfFilterConfig?: CalculatedFieldsQuery) { + this.cfFilterForm.patchValue({ + name: cfFilterConfig?.name ?? [], + types: cfFilterConfig?.types ?? [], + entityType: cfFilterConfig?.entityType ?? null, + entities: cfFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private cfConfigUpdated(formValue: any) { + this.cfFilterConfig = this.cfFilterFromFormValue(formValue); + this.propagateChange(this.cfFilterConfig); + } + + private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery { + return { + name: formValue?.name ?? [], + types: formValue?.types ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html new file mode 100644 index 0000000000..77a40a5283 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss new file mode 100644 index 0000000000..ee0899b5ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts new file mode 100644 index 0000000000..f0824347ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; + +@Component({ + selector: 'tb-calculated-fields-table-header', + templateUrl: './calculated-fields-header.component.html', + styleUrls: ['./calculated-fields-header.component.scss'] +}) +export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent { + + get calculatedFieldsTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfig as CalculatedFieldsTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + calculatedFieldsFilterChanged(calculatedFieldFilterConfig: CalculatedFieldsQuery) { + this.calculatedFieldsTableConfig.calculatedFieldFilterConfig = calculatedFieldFilterConfig; + this.calculatedFieldsTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts index 53a41027bf..1154fc83b6 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts @@ -14,28 +14,15 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; +import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; -import { Observable } from 'rxjs'; -import { OAuth2Service } from '@core/http/oauth2.service'; import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; import { AlarmsMode } from '@shared/models/alarm.models'; import { MenuId } from '@core/services/menu.models'; import { RouterTabsComponent } from "@home/components/router-tabs.component"; import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; -@Injectable() -export class OAuth2LoginProcessingUrlResolver { - - constructor(private oauth2Service: OAuth2Service) { - } - - resolve(): Observable { - return this.oauth2Service.getLoginProcessingUrl(); - } -} - const routes: Routes = [ { path: 'alarms', diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts new file mode 100644 index 0000000000..0ed435075c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; + +const routes: Routes = [ + { + path: 'calculatedFields', + component: CalculatedFieldsTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'entity.type-calculated-fields', + breadcrumb: { + menuId: MenuId.calculated_fields + }, + isPage: true, + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) +export class CalculatedFieldsRoutingModule { } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts new file mode 100644 index 0000000000..01ed3577e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module'; + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule, + CalculatedFieldsRoutingModule + ] +}) +export class CalculatedFieldsModule { } \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 5bb3954cae..c43b89d141 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -27,6 +27,7 @@ import { UserModule } from '@modules/home/pages/user/user.module'; import { DeviceModule } from '@modules/home/pages/device/device.module'; import { AssetModule } from '@modules/home/pages/asset/asset.module'; import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module'; +import { CalculatedFieldsModule } from '@home/pages/calculated-fields/calculated-fields.module'; import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; @@ -69,6 +70,7 @@ import { AiModelModule } from '@home/pages/ai-model/ai-model.module'; EdgeModule, EntityViewModule, CustomerModule, + CalculatedFieldsModule, RuleChainModule, WidgetLibraryModule, DashboardModule, diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index a21d12dc05..5f07374cf8 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -119,6 +119,8 @@ export const CalculatedFieldTypeTranslations = new Map type !== CalculatedFieldType.ALARM) + export type CalculatedFieldConfiguration = | CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration @@ -567,6 +569,8 @@ export type CalculatedFieldArgumentEventValue = CalculatedF export type CalculatedFieldEventArguments = Record>; +export const calculatedFieldsEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = { type: OutputType.Timeseries, strategy: { @@ -1062,8 +1066,8 @@ export function uniqueNameValidator(existingNames: string[]): ValidatorFn { } export interface CalculatedFieldsQuery { - type: CalculatedFieldType; + types: Array; entityType?: EntityType; entities?: Array; - name?: string; + name?: Array; } 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 87f1b24b38..3ed101e779 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1220,6 +1220,11 @@ "deduplication-interval": "Deduplication interval", "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.", "deduplication-interval-required": "Deduplication interval is required.", + "calculated-fields-filter": "Calculated fields filter", + "calculated-field-names": "Calculated field names", + "calculated-field-types": "Calculated field types", + "any-type": "Any type", + "all-names": "All", "metrics": { "metrics": "Metrics", "metrics-empty": "At least one metric must be configured.", From ca71ced6833231d564b3d2fd2c07e31a35442e42 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 16 Dec 2025 18:04:14 +0200 Subject: [PATCH 116/147] KafkaEdgeGrpcSession - move commit outside try/catch to propagate expection in case consumer already closed --- .../server/service/edge/rpc/KafkaEdgeGrpcSession.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index 63669a8e3d..e6eaec23e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -82,16 +82,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { edgeEvents.add(edgeEvent); } List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); + boolean isInterrupted = true; try { - boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); + isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); if (isInterrupted) { log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId()); - } else { - consumer.commit(); } } catch (Exception e) { log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e); } + if (!isInterrupted) { + consumer.commit(); + } } @Override From 1518aa260b9b39c937212c0a47bc895ee0a86312 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 17 Dec 2025 12:27:10 +0200 Subject: [PATCH 117/147] UI: Add calculated fields page actions --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- .../calculated-fields-table-config.ts | 57 +++++++++++++------ ...lated-field-arguments-table.component.html | 3 +- ...culated-field-arguments-table.component.ts | 4 ++ .../calculated-field-dialog.component.html | 12 +++- .../calculated-field-dialog.component.ts | 52 +++++++++-------- ...eofencing-zone-groups-table.component.html | 6 +- ...-geofencing-zone-groups-table.component.ts | 5 ++ ...culated-field-metrics-table.component.html | 6 +- ...alculated-field-metrics-table.component.ts | 5 ++ .../calculated-field-output.component.html | 4 +- .../calculated-field-output.component.ts | 3 + .../propagation-configuration.component.html | 4 +- .../propagation-configuration.component.ts | 3 + .../simple-configuration.component.html | 4 +- .../simple-configuration.component.ts | 5 +- ui-ngx/src/form.scss | 10 ++++ 17 files changed, 129 insertions(+), 56 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index a97acf344e..e573cade2c 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -634,7 +634,7 @@ export const menuSectionMap = new Map([ name: 'entity.type-calculated-fields', type: 'link', path: '/calculatedFields', - icon: 'calculate', + icon: 'mdi:plus-minus-variant', } ], [ diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 7d691c6fc8..2ac2f05bf4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -57,7 +57,7 @@ import { CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { getEntityDetailsPageURL, isObject } from '@core/utils'; +import { deepClone, getEntityDetailsPageURL, isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; @@ -73,7 +73,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.openDebugEventsDialog.call(this, calculatedField), + action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, null, calculatedField), }; calculatedFieldFilterConfig: CalculatedFieldsQuery; @@ -96,6 +96,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + this.editCalculatedField($event, entity); + return true; + }; } this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); this.detailsPanelEnabled = false; @@ -152,17 +157,23 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, + onAction: ($event, entity) => this.copyCalculatedField($event, entity), + }, { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportCalculatedField(event$, entity), + onAction: ($event, entity) => this.exportCalculatedField($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), }, { name: '', @@ -176,7 +187,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - onAction: (_, entity) => this.editCalculatedField(entity), + onAction: ($event, entity) => this.editCalculatedField($event, entity), } ); } @@ -198,14 +209,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -223,7 +232,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (res) { @@ -233,13 +243,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + const entityId = this.entityId || value?.entityId; return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, tenantId: this.tenantId, entityName: this.entityName, ownerId: this.ownerId, @@ -253,7 +264,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { return (calculatedField.type === CalculatedFieldType.SCRIPT || (calculatedField.type === CalculatedFieldType.PROPAGATION && @@ -285,12 +297,25 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + if (res) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog() .pipe( @@ -376,7 +401,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (openCalculatedFieldEdit) { - this.editCalculatedField({ + this.editCalculatedField(null, { entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression} as any }, true) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index c6f2a5d479..99b4225db2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -111,6 +111,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base"> {{ 'calculated-fields.no-arguments' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index 002a515164..9a8395a557 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -157,6 +157,10 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces return this.errorText ? { argumentsFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, argument: CalculatedFieldArgumentValue): void { $event.stopPropagation(); const index = this.argumentsFormArray.controls.findIndex(control => isEqual(control.value, argument)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index ac089bb4b1..44c2e702e5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -21,6 +21,7 @@
@@ -93,6 +95,7 @@ @switch (fieldFormGroup.get('type').value) { @case (CalculatedFieldType.GEOFENCING) { {{ 'action.cancel' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 967e931ded..4d2df6e416 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -42,7 +42,7 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit import { deepTrim, isDefined } from '@core/utils'; import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; -import { EntityService } from '@core/http/entity.service'; +import { BaseData } from '@shared/models/base-data'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -82,6 +82,9 @@ export class CalculatedFieldDialogComponent extends DialogComponent, private calculatedFieldsService: CalculatedFieldsService, - private entityService: EntityService, private destroyRef: DestroyRef, private fb: FormBuilder) { super(store, router, dialogRef); - this.observeIsLoading(); this.observeType(); this.applyDialogData(); - if (!this.entityName) { + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + + if (!this.data.entityId) { this.fieldFormGroup.get('entityId.id').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((entityId) => { - if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE || - this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) { - this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe( - value => { - this.entityName = value.name; - } - ) + this.disabledConfiguration = !entityId; + if (this.disabledConfiguration) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + } else { + this.fieldFormGroup.get('configuration').enable({emitEvent: false}); } }); } @@ -130,9 +133,13 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); } else { this.fieldFormGroup.get('name').markAsTouched(); this.entityTypeSelect?.markAsTouched(); @@ -155,6 +162,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent): void { + this.entityName = entity.name; + } + get entityId(): EntityId { return this.data.entityId || this.fieldFormGroup.get('entityId').value; } @@ -168,19 +179,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); - } - - private observeIsLoading(): void { - this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { - if (loading) { - this.fieldFormGroup.disable({emitEvent: false}); - } else { - this.fieldFormGroup.enable({emitEvent: false}); - if (this.data.isDirty) { - this.fieldFormGroup.markAsDirty(); - } - } - }); + if (!this.data.entityId) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + this.disabledConfiguration = true; + } } private observeType(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index 20db5c732d..fa0ba32f22 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -101,6 +101,7 @@ #button (click)="manageZone($event, button, geofenceZone)" [matTooltip]="'action.edit' | translate" + [disabled]="disable" matTooltipPosition="above"> delete @@ -128,7 +130,7 @@ }
+ class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable"> {{ 'calculated-fields.no-zone-configured' | translate }}
@@ -137,7 +139,7 @@ color="primary" #button (click)="manageZone($event, button)" - [disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF"> + [disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF || disable"> {{ 'calculated-fields.add-zone-group' | translate }} @if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts index b3e5edb80a..228d08fd90 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts @@ -90,6 +90,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro entityNameMap = new Map(); sortOrder = { direction: 'asc', property: '' }; dataSource = new CalculatedFieldZoneDatasource(); + disable = false; readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; readonly entityTypeTranslations = entityTypeTranslations; @@ -135,6 +136,10 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro return this.errorText ? { zonesFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { $event.stopPropagation(); const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index a35dc97d8a..4693d3afb4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -97,6 +97,7 @@ mat-icon-button #button (click)="manageMetrics($event, button, metric)" + [disabled]="disable" [matTooltip]="'action.edit' | translate" matTooltipPosition="above"> edit @@ -104,6 +105,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable"> {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
@@ -125,7 +127,7 @@ color="primary" #button (click)="manageMetrics($event, button)" - [disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF"> + [disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF || disable"> {{ 'calculated-fields.metrics.add-metric' | translate }} @if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index 78021529d6..21b21d07ac 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -89,6 +89,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu metricsFormArray = this.fb.array([]); sortOrder = { direction: 'asc' as SortDirection, property: '' }; dataSource = new CalculatedFieldMetricsDatasource(); + disable = false; displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; @@ -141,6 +142,10 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu return this.errorText ? { metricsFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { $event.stopPropagation(); const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 3a47c00b1c..c2c989d0c8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -78,7 +78,7 @@
-
+
{{ 'calculated-fields.output-strategy.strategy' | translate }}
@@ -90,7 +90,7 @@ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { -
+
calculated-fields.output-strategy.processing-parameters
@if (outputForm.get('type').value === OutputType.Timeseries) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 239a4921e4..149b5aa3b9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -78,6 +78,8 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val @Input({required: true}) entityId: EntityId; + disabled = false; + readonly outputTypes = Object.values(OutputType) as OutputType[]; readonly OutputType = OutputType; readonly AttributeScope = AttributeScope; @@ -171,6 +173,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.outputForm.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index bd92d25f92..6dc5702735 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -80,7 +80,7 @@ matTooltip="{{ 'calculated-fields.test-expression-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="propagateConfiguration.get('arguments').invalid" + [disabled]="propagateConfiguration.get('arguments').invalid || disabled" (click)="onTestScript()"> bug_report @@ -89,7 +89,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index bce244c7ba..1c493148b3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -88,6 +88,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, output: this.fb.control(defaultCalculatedFieldOutput), }); + disabled = false; + readonly ScriptLanguage = ScriptLanguage; readonly CalculatedFieldType = CalculatedFieldType; readonly OutputType = OutputType; @@ -142,6 +144,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.propagateConfiguration.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html index 178f8b47d6..e6a37043d5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -72,7 +72,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="simpleConfiguration.get('arguments').invalid" + [disabled]="simpleConfiguration.get('arguments').invalid || disabled" (click)="onTestScript()"> bug_report @@ -81,7 +81,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 8dc6f01726..e6ff1d1bad 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -104,6 +104,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) ); + disabled = false; + private propagateChange: (config: SimpeConfiguration) => void = () => { }; constructor(private fb: FormBuilder) { @@ -127,7 +129,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (propName === 'isScript') { + if (propName === 'isScript' && !this.disabled) { this.updatedFormWithScript(); if (!change.firstChange) { this.simpleConfiguration.updateValueAndValidity(); @@ -163,6 +165,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.simpleConfiguration.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index b8e269c14e..f300b5e42b 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -107,6 +107,9 @@ } } } + &.disabled { + color: var(--mdc-outlined-text-field-disabled-input-text-color); + } .mat-expansion-panel { &.tb-settings { box-shadow: none; @@ -160,6 +163,13 @@ } } + .tb-form-panel-disabled { + color: var(--mdc-outlined-text-field-disabled-input-text-color); + .tb-form-panel { + color: var(--mdc-outlined-text-field-disabled-input-text-color); + } + } + .tb-form-panel-title { font-weight: 500; font-size: 16px; From e0357aff503e71a0fa0e32f2d59d626da70a0000 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 17 Dec 2025 12:44:59 +0200 Subject: [PATCH 118/147] fixed processing telemetry batch --- ...CalculatedFieldEntityMessageProcessor.java | 34 +++++++- .../cf/ctx/state/TsRollingArgumentEntry.java | 32 +++++--- .../cf/CalculatedFieldIntegrationTest.java | 80 +++++++++++++++++++ 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 49d1af2b0f..257e9d8565 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -48,6 +49,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; import java.util.ArrayList; import java.util.Collection; @@ -62,6 +64,8 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord; + /** * @author Andrew Shvayka @@ -356,13 +360,39 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); Set argNames = args.get(key); if (argNames != null) { - argNames.forEach(argName -> arguments.put(argName, new SingleValueArgumentEntry(item))); + SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item); + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing == null) { + return incoming; + } + existing.updateEntry(incoming); + return existing; + })); } key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); argNames = args.get(key); if (argNames != null) { - argNames.forEach(argName -> arguments.put(argName, new SingleValueArgumentEntry(item))); + Double recordValue = getValueForTsRecord(ProtoUtils.fromProto(item.getKv())); + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing instanceof TsRollingArgumentEntry rolling) { + if (recordValue != null) { + rolling.getTsRecords().put(item.getTs(), recordValue); + } + return rolling; + } + TsRollingArgumentEntry rolling = new TsRollingArgumentEntry(); + if (recordValue != null) { + rolling.getTsRecords().put(item.getTs(), recordValue); + } + if (existing instanceof SingleValueArgumentEntry single) { + Double existingValue = getValueForTsRecord(single.getKvEntryValue()); + if (existingValue != null) { + rolling.getTsRecords().put(single.getTs(), existingValue); + } + } + return rolling; + })); } } return arguments; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index e01d8b7369..9f9ad5db73 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -122,20 +122,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry { } private void addTsRecord(Long ts, KvEntry value) { - try { - switch (value.getDataType()) { - case LONG -> value.getLongValue().ifPresent(aLong -> tsRecords.put(ts, aLong.doubleValue())); - case DOUBLE -> value.getDoubleValue().ifPresent(aDouble -> tsRecords.put(ts, aDouble)); - case BOOLEAN -> value.getBooleanValue().ifPresent(aBoolean -> tsRecords.put(ts, aBoolean ? 1.0 : 0.0)); - case STRING -> value.getStrValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString))); - case JSON -> value.getJsonValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString))); - } - } catch (Exception e) { - tsRecords.put(ts, Double.NaN); - log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue()); - } finally { - cleanupExpiredRecords(); + Double recordValue = getValueForTsRecord(value); + if (recordValue != null) { + tsRecords.put(ts, recordValue); } + cleanupExpiredRecords(); } private void addTsRecord(Long ts, double value) { @@ -150,4 +141,19 @@ public class TsRollingArgumentEntry implements ArgumentEntry { tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow); } + public static Double getValueForTsRecord(KvEntry value) { + try { + return switch (value.getDataType()) { + case LONG -> value.getLongValue().map(Long::doubleValue).orElse(null); + case DOUBLE -> value.getDoubleValue().orElse(null); + case BOOLEAN -> value.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElse(null); + case STRING -> value.getStrValue().map(Double::parseDouble).orElse(null); + case JSON -> value.getJsonValue().map(Double::parseDouble).orElse(null); + }; + } catch (Exception e) { + log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue()); + return Double.NaN; + } + } + } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c83e8c5ee3..1da3673695 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -796,6 +796,86 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":5}")); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":10}")); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}")); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SCRIPT); + calculatedField.setName("Script CF"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + Argument argumentA = new Argument(); + argumentA.setRefEntityKey(refEntityKeyA); + Argument argumentB = new Argument(); + ReferencedEntityKey refEntityKeyB = new ReferencedEntityKey("b", ArgumentType.TS_ROLLING, null); + argumentB.setTimeWindow(TimeUnit.MINUTES.toMillis(10)); + argumentB.setLimit(1000); + argumentB.setRefEntityKey(refEntityKeyB); + config.setArguments(Map.of("a", argumentA, "b", argumentB)); + config.setExpression(""" + return { + "latestA": a, + "avgB": b.avg + }; + """); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB"); + assertThat(result).isNotNull(); + assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("5"); + assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0"); + }); + + long now = System.currentTimeMillis(); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format(""" + [{ + "ts": %s, + "values": { + "a": 6, + "b": 100 + } + }, { + "ts": %s, + "values": { + "a": 7, + "b": 200 + } + }, { + "ts": %s, + "values": { + "a": 8, + "b": 300 + } + }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5)))); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB"); + assertThat(result).isNotNull(); + assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("7"); + assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("126.0"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } From 3f0388b7c0cf7737ebf0b5dc9c73f8833003f113 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Dec 2025 12:15:23 +0100 Subject: [PATCH 119/147] TbMqttSslTransportComponent for MqttSslHandlerProvider --- .../mqtt/MqttSslHandlerProvider.java | 3 +- .../mqtt/TbMqttSslTransportComponent.java | 31 +++++++++++++++++++ .../mqtt/TbMqttTransportComponent.java | 6 +++- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 867b39cd23..77396c69fa 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @@ -48,7 +47,7 @@ import java.util.concurrent.TimeUnit; @Slf4j @Component("MqttSslHandlerProvider") -@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) +@TbMqttSslTransportComponent public class MqttSslHandlerProvider { @Value("${transport.mqtt.ssl.protocol}") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java new file mode 100644 index 0000000000..2a0e8ef42f --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Same as @TbMqttTransportComponent with additional condition by `transport.mqtt.ssl.enabled == true` + */ + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true' && '${transport.mqtt.ssl.enabled:false}'=='true')") +public @interface TbMqttSslTransportComponent {} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java index b517cda103..0d034aebb2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java @@ -21,7 +21,11 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * See also @TbMqttSslTransportComponent + */ + @Inherited @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true')") public @interface TbMqttTransportComponent {} From 6803b38207783e74b51106faa71df33b9a39466a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 17 Dec 2025 14:19:57 +0200 Subject: [PATCH 120/147] merge with master --- .../RelatedEntitiesAggregationCalculatedFieldState.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index 7c12f74b8a..1edfea70a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -178,7 +178,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat }); lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; lastArgsRefreshTs = System.currentTimeMillis(); - readinessStatus = checkReadiness(requiredArguments, arguments); + readinessStatus = checkReadiness(); } public void scheduleReevaluation() { @@ -291,12 +291,12 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat record EntityArgument(EntityInfo entity, JsonNode entityArguments) {} @Override - protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { - if (currentArguments == null) { + protected ReadinessStatus checkReadiness() { + if (arguments == null) { return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); } for (String requiredArgumentKey : requiredArguments) { - ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); if (argumentEntry == null || argumentEntry.isEmpty()) { return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); } From cb2f4080787a6eb9d8c899e3bcf7ca22ec34894d Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 17 Dec 2025 15:21:30 +0200 Subject: [PATCH 121/147] UI: Fixed entityId and add row click --- .../alarm-rule-dialog.component.html | 25 +++++---- .../alarm-rule-dialog.component.ts | 54 ++++++++++++------- .../alarm-rule-filter-config.component.ts | 4 +- .../alarm-rules/alarm-rules-table-config.ts | 41 ++++++++------ .../app/shared/models/alarm-rule.models.ts | 2 + ui-ngx/src/app/shared/models/constants.ts | 3 +- 6 files changed, 80 insertions(+), 49 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 52ff7d15cb..d9bff11bf0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -19,7 +19,9 @@

{{ 'alarm-rule.alarm-rule' | translate}}

+
}
-
-
{{ 'calculated-fields.arguments' | translate }}
+
+
{{ 'calculated-fields.arguments' | translate }}
-
-
{{ 'alarm-rule.create-conditions' | translate }}
+
+
{{ 'alarm-rule.create-conditions' | translate }}
-
-
{{ 'alarm-rule.clear-condition' | translate }}
+
+
{{ 'alarm-rule.clear-condition' | translate }}
@@ -122,7 +124,7 @@
- + {{ 'alarm-rule.advanced-settings' | translate }} @@ -171,13 +173,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index ac4d0f9ed6..a84aa52693 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -30,21 +30,22 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; -import { MatChipInputEvent } from "@angular/material/chips"; import { AlarmRule, AlarmRuleConditionType, + alarmRuleEntityTypeList, AlarmRuleExpressionType, AlarmRuleTestScriptFn } from "@shared/models/alarm-rule.models"; import { deepTrim } from "@core/utils"; -import { Observable } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { combineLatest, Observable } from "rxjs"; +import { debounceTime, startWith, switchMap } from "rxjs/operators"; import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component"; import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component"; import { EntityService } from "@core/http/entity.service"; import { RelationTypes } from "@shared/models/relation.models"; import { StringItemsOption } from "@shared/components/string-items-list.component"; +import { BaseData } from "@shared/models/base-data"; export interface AlarmRuleDialogData { value?: CalculatedField; @@ -92,7 +93,7 @@ export class AlarmRuleDialogComponent extends DialogComponent { this.updateRulesValidators(); }); - if (!this.entityName) { - this.fieldFormGroup.get('entityId.id').valueChanges.pipe( - takeUntilDestroyed(this.destroyRef) - ).subscribe((entityId) => { - if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE || - this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) { - this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe( - value => { - this.entityName = value.name; - } - ) + if (!this.data.entityId) { + combineLatest([ + this.fieldFormGroup.get('entityId.id')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId.id')!.value)), + this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value)) + ]).pipe( + debounceTime(50), + takeUntilDestroyed() + ).subscribe(([entityId, name]) => { + this.disabledArguments = !entityId || !name?.length; + const argsControl = this.fieldFormGroup.get('configuration.arguments')!; + if (this.disabledArguments) { + argsControl.disable({ emitEvent: false }); + } else { + argsControl.enable({ emitEvent: false }); } }); } @@ -174,12 +184,16 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { + this.isLoading = true; const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; alarmRule.configuration.type = CalculatedFieldType.ALARM; this.calculatedFieldsService.saveCalculatedField(alarmRule) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(calculatedField => this.dialogRef.close(calculatedField)); + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); } else { this.fieldFormGroup.get('name').markAsTouched(); this.entityTypeSelect?.markAsTouched(); @@ -215,7 +229,7 @@ export class AlarmRuleDialogComponent extends DialogComponent): void { + this.entityName = entity.name; + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts index a19fc688a8..da91aafb07 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts @@ -39,7 +39,7 @@ import { fromEvent, Subscription } from 'rxjs'; import { POSITION_MAP } from '@shared/models/overlay.models'; import { UtilsService } from '@core/services/utils.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AlarmRuleFilterConfig } from "@shared/models/alarm-rule.models"; +import { alarmRuleEntityTypeList, AlarmRuleFilterConfig } from "@shared/models/alarm-rule.models"; export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmRuleFilterConfigData'); @@ -100,7 +100,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces entityType = EntityType; - listEntityTypes = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + listEntityTypes = alarmRuleEntityTypeList; entityTypeTranslations = entityTypeTranslations; private alarmRuleFilterConfig: AlarmRuleFilterConfig; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index b242fd9701..3784bc09bf 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -72,7 +72,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('calculated-fields.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, null, calculatedField), }; alarmRuleFilterConfig: CalculatedFieldsQuery; @@ -95,6 +95,13 @@ export class AlarmRulesTableConfig extends EntityTableConfig { super(); if (this.pageMode) { this.headerComponent = AlarmRuleTableHeaderComponent; + + this.rowPointer = true; + + this.handleRowClick = ($event, model) => { + this.editCalculatedField($event, model); + return true; + }; } this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); this.detailsPanelEnabled = false; @@ -153,7 +160,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('alarm-rule.copy'), icon: 'content_copy', isEnabled: () => true, - onAction: ($event, entity) => this.copyCalculatedField(entity) + onAction: ($event, entity) => this.copyCalculatedField($event, entity) } ); this.cellActionDescriptors.push( @@ -161,13 +168,13 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportAlarmRule(event$, entity), + onAction: ($event, entity) => this.exportAlarmRule($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), }, { name: '', @@ -181,7 +188,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: (_, entity) => this.editCalculatedField(entity), + onAction: ($event, entity) => this.editCalculatedField($event, entity), } ); } @@ -193,14 +200,12 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { ...this.additionalDebugActionConfig, - action: () => this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -219,7 +224,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }, $event.target as Element); } - private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + private editCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + $event?.stopPropagation(); this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) .subscribe((res) => { if (res) { @@ -228,7 +234,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }); } - private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + private copyCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + $event?.stopPropagation(); const copyCalculatedAlarmRule = deepClone(calculatedField); if (this.pageMode) { copyCalculatedAlarmRule.entityId = null; @@ -243,13 +250,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { + const entityId = this.entityId || value?.entityId; return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, tenantId: this.tenantId, entityName: this.entityName, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, @@ -263,7 +271,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { .pipe(filter(Boolean)); } - private openDebugEventsDialog(calculatedField: CalculatedField): void { + private openDebugEventsDialog($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], @@ -282,9 +291,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { - if ($event) { - $event.stopPropagation(); - } + $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); } @@ -361,7 +368,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { filter(Boolean), tap(expression => { if (openCalculatedFieldEdit) { - this.editCalculatedField({ + this.editCalculatedField(null, { entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression} as any }, true) diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts index 3accaed95c..df137313be 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -23,6 +23,8 @@ import { EntityType } from "@shared/models/entity-type.models"; import { Observable } from "rxjs"; import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +export const alarmRuleEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + export enum AlarmRuleScheduleType { ANY_TIME = 'ANY_TIME', SPECIFIC_TIME = 'SPECIFIC_TIME', diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 2788aa52df..9253c0004a 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -219,7 +219,8 @@ export const HelpLinks = { aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`, apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/api-keys`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, - trendzSettings: `${helpBaseUrl}/docs/trendz/` + trendzSettings: `${helpBaseUrl}/docs/trendz/`, + alarmRules: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/alarm-rules/`, } }; /* eslint-enable max-len */ From b06dd754bc97be9b98da5f40065664cd5cd6847c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 17 Dec 2025 14:08:12 +0200 Subject: [PATCH 122/147] UI: Updated filter and calculated fields page icon --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- .../calculated-fields-table-config.ts | 32 +++---------------- ...ulated-fields-filter-config.component.html | 11 ------- ...lculated-fields-filter-config.component.ts | 7 ---- .../assets/locale/locale.constant-en_US.json | 3 +- 5 files changed, 6 insertions(+), 49 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index e573cade2c..f42943cd93 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -634,7 +634,7 @@ export const menuSectionMap = new Map([ name: 'entity.type-calculated-fields', type: 'link', path: '/calculatedFields', - icon: 'mdi:plus-minus-variant', + icon: 'mdi:function-variant', } ], [ diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 2ac2f05bf4..c8732eddf3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -99,6 +99,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { this.editCalculatedField($event, entity); + this.rowPointer = true; return true; }; } @@ -131,30 +132,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '250px'); - expressionColumn.sortable = false; - expressionColumn.cellContentFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; - } - expressionColumn.cellTooltipFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? null : expressionLabel - }; - this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '30%' :'33%', + this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '33%' : '60%', entity => this.utilsService.customTranslation(entity.name, entity.name))); - this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); - this.columns.push(expressionColumn); - if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', - entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.target-entity', '30%', + this.columns.push(new EntityLinkTableColumn('entityName', 'calculated-fields.target-entity', '33%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } + this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '33%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.cellActionDescriptors.push( { @@ -192,16 +178,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { return this.pageMode ? this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, this.calculatedFieldFilterConfig): diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html index 6e67b9581b..b3cc3b387a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html @@ -56,17 +56,6 @@
-
-
calculated-fields.calculated-field-names
- - -
calculated-fields.calculated-field-types
Date: Wed, 17 Dec 2025 16:55:39 +0200 Subject: [PATCH 123/147] Clear code --- .../components/output/calculated-field-output.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index c2c989d0c8..5bf7788e10 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -90,7 +90,7 @@ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { -
+
calculated-fields.output-strategy.processing-parameters
@if (outputForm.get('type').value === OutputType.Timeseries) { From 0bd2651f223c067d80b9b169a2bf91ccaaecdbef Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 17 Dec 2025 17:11:02 +0200 Subject: [PATCH 124/147] Fixed generation of startTs for new alarms in AlarmCalculatedFieldState --- .../server/service/cf/ctx/state/BaseCalculatedFieldState.java | 2 -- .../service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 754fc2f6ee..09ca35cc4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -48,7 +48,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, protected Map arguments = new HashMap<>(); protected boolean sizeExceedsLimit; - protected long latestTimestamp = DEFAULT_LAST_UPDATE_TS; protected ReadinessStatus readinessStatus; @Setter @@ -121,7 +120,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, requiredArguments = null; arguments.clear(); sizeExceedsLimit = false; - latestTimestamp = DEFAULT_LAST_UPDATE_TS; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index 9dd944c2e4..4f2cbede09 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -356,7 +356,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { newAlarm.setAcknowledged(false); newAlarm.setCleared(false); newAlarm.setSeverity(severity); - long startTs = latestTimestamp; + long startTs = getLatestTimestamp(); long currentTime = System.currentTimeMillis(); if (startTs <= 0L || startTs > currentTime) { startTs = currentTime; From 6d945e3fe6d7fbe8863c0f47a79e737d296ceec9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 17 Dec 2025 18:08:09 +0200 Subject: [PATCH 125/147] UI: Improved text to show debug events in panel --- .../home/components/alarm-rules/alarm-rules-table-config.ts | 2 +- .../calculated-fields/calculated-fields-table-config.ts | 2 +- .../modules/home/pages/rulechain/rulechain-page.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index b242fd9701..e9ba2dd92f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -71,7 +71,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { - title: this.translate.instant('calculated-fields.see-debug-events'), + title: this.translate.instant('action.see-debug-events'), action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), }; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index c8732eddf3..56a3cc2a45 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -72,7 +72,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.openDebugEventsDialog.call(this, null, calculatedField), }; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 7f751a0bfe..ac011bccf7 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1660,7 +1660,7 @@ export class RuleChainPageComponent extends PageComponent get additionalActionConfig (): AdditionalDebugActionConfig { return { - title: this.translate.instant('rulenode.rule-node-events'), + title: this.translate.instant('action.see-debug-events'), action: this.switchToEventsTab.bind(this) } } 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 960d3d3857..6495a4a480 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -78,6 +78,7 @@ "show-more": "Show more", "dont-show-again": "Do not show again", "see-documentation": "See documentation", + "see-debug-events": "See debug events", "clear": "Clear", "upload": "Upload", "delete-anyway": "Delete anyway", @@ -1132,7 +1133,6 @@ "argument-owner": "Current owner", "argument-relation-query": "Related entities", "argument-type": "Argument type", - "see-debug-events": "See debug events", "attribute": "Attribute", "copy-argument-name": "Copy argument name", "timeseries-key": "Time series key", From 68b96e576c5abcac825e4e5c1055ea1c08c8e9ae Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 18 Dec 2025 08:53:36 +0200 Subject: [PATCH 126/147] added clear error message when invalid attribute scope --- .../telemetry/TbCalculatedFieldsNode.java | 23 +- .../telemetry/TbCalculatedFieldsNodeTest.java | 198 ++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index 9d91802f8b..ffc9e54d24 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -25,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; @@ -101,11 +102,15 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.tellSuccess(msg); return; } + AttributeScope scope = resolveScope(ctx, msg); + if (scope == null) { + return; + } AttributesSaveRequest attributesSaveRequest = AttributesSaveRequest.builder() .tenantId(ctx.getTenantId()) .entityId(msg.getOriginator()) - .scope(AttributeScope.valueOf(msg.getMetaData().getValue(SCOPE))) + .scope(scope) .entries(newAttributes) .strategy(AttributesSaveRequest.Strategy.CF_ONLY) .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) @@ -116,4 +121,20 @@ public class TbCalculatedFieldsNode implements TbNode { ctx.getTelemetryService().saveAttributes(attributesSaveRequest); } + private AttributeScope resolveScope(TbContext ctx, TbMsg msg) { + String scopeStr = msg.getMetaData().getValue(SCOPE); + + if (StringUtils.isEmpty(scopeStr)) { + ctx.tellFailure(msg, new IllegalArgumentException("Attribute scope is missing")); + return null; + } + + try { + return AttributeScope.valueOf(scopeStr); + } catch (IllegalArgumentException e) { + ctx.tellFailure(msg, new IllegalArgumentException("Invalid attribute scope: " + scopeStr)); + return null; + } + } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java new file mode 100644 index 0000000000..755f282dde --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNodeTest.java @@ -0,0 +1,198 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.telemetry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +class TbCalculatedFieldsNodeTest { + + final TenantId tenantId = TenantId.fromUUID(UUID.fromString("7361ca62-7688-4d78-b374-bc3d77e12dba")); + final DeviceId deviceId = new DeviceId(UUID.fromString("21c55f8d-0c5c-47b3-a344-9657e194b0f6")); + + @Spy + TbCalculatedFieldsNode node; + + @Mock + TbContext ctxMock; + @Mock + RuleEngineTelemetryService telemetryServiceMock; + + @BeforeEach + void setUp() { + lenient().when(ctxMock.getTenantId()).thenReturn(tenantId); + lenient().when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock); + var config = new EmptyNodeConfiguration().defaultConfiguration(); + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @ParameterizedTest + @EnumSource(TbMsgType.class) + void givenInvalidMsgType_whenOnMsg_thenTellFailure(TbMsgType msgType) { + if (TbMsgType.POST_TELEMETRY_REQUEST == msgType || TbMsgType.POST_ATTRIBUTES_REQUEST == msgType) { + return; + } + + // GIVEN + var msg = TbMsg.newMsg() + .type(msgType) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported msg type: " + msg.getType()); + } + + @Test + void givenTelemetryMsg_whenOnMsg_thenPushToTelemetryService() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(telemetryServiceMock).should().saveTimeseries(assertArg( + actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.CF_ONLY) + )); + } + + @ParameterizedTest + @ValueSource(strings = {"POST_TELEMETRY_REQUEST", "POST_ATTRIBUTES_REQUEST"}) + void givenEmptyTelemetryOrAttributesMsg_whenOnMsg_thenTellSuccess(String msgType) { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.valueOf(msgType)) + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().tellSuccess(msg); + } + + @Test + void givenEmptyAttributeScope_whenOnMsg_thenTellFailure() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attribute scope is missing"); + } + + @Test + void givenInvalidAttributeScope_whenOnMsg_thenTellFailure() { + // GIVEN + String invalidScope = "INVALID_SCOPE"; + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(new TbMsgMetaData(Map.of("scope", invalidScope))) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + ArgumentCaptor actualError = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().tellFailure(eq(msg), actualError.capture()); + assertThat(actualError.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid attribute scope: " + invalidScope); + } + + @Test + void givenAttributesMsg_whenOnMsg_thenPushToTelemetryService() { + // GIVEN + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_ATTRIBUTES_REQUEST) + .originator(deviceId) + .data(JacksonUtil.newObjectNode().put("active", true).toString()) + .metaData(new TbMsgMetaData(Map.of("scope", AttributeScope.SERVER_SCOPE.name()))) + .build(); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(telemetryServiceMock).should().saveAttributes(assertArg( + actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(AttributesSaveRequest.Strategy.CF_ONLY) + )); + } + +} From 17761480c03d9373dac8145dea227b71e6321c9f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 18 Dec 2025 16:15:54 +0200 Subject: [PATCH 127/147] fixed sorting by originatorDisplayName --- .../java/org/thingsboard/server/dao/model/ModelConstants.java | 2 ++ .../thingsboard/server/dao/sql/query/AlarmDataAdapter.java | 4 +++- .../server/dao/sql/query/DefaultAlarmQueryRepository.java | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 0cd80c0f3b..2c17dd0686 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -257,6 +257,8 @@ public class ModelConstants { public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; + + public static final String ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY = "originator_display_name"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String ALARM_ASSIGNEE_FIRST_NAME_PROPERTY = "assignee_first_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index f3e2ba83a6..f701188064 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -117,11 +117,13 @@ public class AlarmDataAdapter { String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; Object originatorLabelObj = row.get(ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); String originatorLabel = originatorLabelObj != null ? originatorLabelObj.toString() : null; + Object originatorDisplayNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); + String originatorDisplayName = originatorDisplayNameObj != null ? originatorDisplayNameObj.toString() : null; AlarmData alarmData = new AlarmData(alarm, entityId); alarmData.setOriginatorName(originatorName); alarmData.setOriginatorLabel(originatorLabel); - alarmData.setOriginatorDisplayName(StringUtils.isBlank(originatorLabel) ? originatorName : originatorLabel); + alarmData.setOriginatorDisplayName(originatorDisplayName); if (alarm.getAssigneeId() != null) { alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 62636a9ed0..1eec369795 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -83,6 +83,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { alarmFieldColumnMap.put(ASSIGNEE_FIRST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_LAST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_EMAIL_KEY, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); + alarmFieldColumnMap.put("originatorDisplayName", ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); } private static final String FIELDS_SELECTION = "select a.id as id," + @@ -106,6 +107,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.type as type, " + " a.originator_name as originator_name, " + " a.originator_label as originator_label, " + + " coalesce(a.originator_label, a.originator_label) as originator_display_name, " + " a.assignee_first_name as assignee_first_name, " + " a.assignee_last_name as assignee_last_name, " + " a.assignee_email as assignee_email, " + From dc8d2d896af9b49d353d0ded2428bdc517aa4cc3 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 18 Dec 2025 16:24:31 +0200 Subject: [PATCH 128/147] fixed query --- .../java/org/thingsboard/server/dao/model/ModelConstants.java | 1 - .../server/dao/sql/query/DefaultAlarmQueryRepository.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 2c17dd0686..16493b90fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -257,7 +257,6 @@ public class ModelConstants { public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; - public static final String ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY = "originator_display_name"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 1eec369795..a41961768a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -107,7 +107,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.type as type, " + " a.originator_name as originator_name, " + " a.originator_label as originator_label, " + - " coalesce(a.originator_label, a.originator_label) as originator_display_name, " + + " coalesce(a.originator_label, a.originator_name) as originator_display_name, " + " a.assignee_first_name as assignee_first_name, " + " a.assignee_last_name as assignee_last_name, " + " a.assignee_email as assignee_email, " + From 550c65dc938f6049d73bcd29a292ce6438fe6c4f Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Thu, 18 Dec 2025 17:20:53 +0200 Subject: [PATCH 129/147] Fix DeviceProfile copy constructor --- .../java/org/thingsboard/server/common/data/DeviceProfile.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 6f31cda6aa..7d9b4e1502 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -114,6 +114,9 @@ public class DeviceProfile extends BaseData implements HasName, this.description = deviceProfile.getDescription(); this.image = deviceProfile.getImage(); this.isDefault = deviceProfile.isDefault(); + this.type = deviceProfile.getType(); + this.transportType = deviceProfile.getTransportType(); + this.provisionType = deviceProfile.getProvisionType(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); this.defaultDashboardId = deviceProfile.getDefaultDashboardId(); this.defaultQueueName = deviceProfile.getDefaultQueueName(); From 674c6d6c88148307c113ec080f38cd37026ca9cf Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Thu, 18 Dec 2025 17:22:45 +0200 Subject: [PATCH 130/147] Fix deviceProfile sync for edges with version < 4.3 --- .../service/edge/EdgeMsgConstructorUtils.java | 34 +++++++++++++++++-- .../processor/device/DeviceEdgeProcessor.java | 2 +- .../profile/DeviceProfileEdgeProcessor.java | 3 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 6b50b8ae15..e1c7f1198c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -51,6 +51,8 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; @@ -261,12 +263,40 @@ public class EdgeMsgConstructorUtils { return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build(); } - public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { - return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(deviceProfile)) + public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) { + String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion); + return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity) .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build(); } + public static String getEntityAndFixLwm2mBootstrapShortServerId(DeviceProfile deviceProfile, EdgeVersion edgeVersion) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber()) { + return JacksonUtil.toString(deviceProfile); + } + JsonNode jsonNode = JacksonUtil.valueToTree(deviceProfile); + JsonNode profileDataNode = jsonNode.get("profileData"); + if (profileDataNode != null && profileDataNode.has("transportConfiguration")) { + JsonNode transportConfigNode = profileDataNode.get("transportConfiguration"); + JsonNode bootstrapNode = transportConfigNode.get("bootstrap"); + if (bootstrapNode != null && bootstrapNode.isArray()) { + for (JsonNode bootstrapServerNode : bootstrapNode) { + if (bootstrapServerNode.isObject()) { + ObjectNode serverObjectNode = (ObjectNode) bootstrapServerNode; + JsonNode isBootstrapNode = serverObjectNode.get("bootstrapServerIs"); + boolean isBootstrapServer = isBootstrapNode != null && isBootstrapNode.asBoolean(false); + JsonNode shortServerIdNode = serverObjectNode.get("shortServerId"); + if (isBootstrapServer && (shortServerIdNode == null || shortServerIdNode.isNull())) { + serverObjectNode.put("shortServerId", 0); + } + } + } + } + } + return JacksonUtil.toString(jsonNode); + } + public static DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { return DeviceProfileUpdateMsg.newBuilder() .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 2f655d961b..70b8f80fb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -237,7 +237,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); - builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion)); } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 540fd4b448..6f781d9492 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -102,7 +102,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -141,5 +141,4 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple public EdgeEventType getEdgeEventType() { return EdgeEventType.DEVICE_PROFILE; } - } From f814a52faf82833d8a84672fd86f4719354d296d Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Thu, 18 Dec 2025 17:26:09 +0200 Subject: [PATCH 131/147] Fix synchronization between 'profileData' and 'profileDataBytes' after shortServerId update --- .../dao/service/validator/DeviceProfileDataValidator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 9fac76704c..28d7d598ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -174,6 +174,8 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator Date: Thu, 18 Dec 2025 17:42:28 +0200 Subject: [PATCH 132/147] Added normalization of y-axis limit in chart initialization --- .../widget/lib/chart/time-series-chart.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 8568e4b13b..f52bf13ada 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -581,6 +581,8 @@ export class TbTimeSeriesChart { yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { + yAxisSettings.min = this.normalizeAxisLimit(yAxisSettings.min); + yAxisSettings.max = this.normalizeAxisLimit(yAxisSettings.max); const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); const units = isNotEmptyTbUnits(axisSettings.units) ? axisSettings.units : this.ctx.units; @@ -1078,4 +1080,21 @@ export class TbTimeSeriesChart { this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } } + + private normalizeAxisLimit(limit: string | number | ValueSourceConfig): string | number | ValueSourceConfig { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number' || typeof limit === 'string') { + return { + type: ValueSourceType.constant, + value: Number(limit), + entityAlias: null + }; + } + return limit; + } } From 8bf9e62a8ef06f7d5237b2b4dc22fbb438d19bf6 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 18 Dec 2025 17:44:23 +0200 Subject: [PATCH 133/147] UI: Fixed action color and add new style for table timewindow --- ui-ngx/src/assets/dashboard/api_usage.json | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index cad63bd3e0..f4d5892f61 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -121,7 +121,24 @@ "noDataDisplayMessage": "", "configMode": "basic", "borderRadius": "12px", - "titleColor": "rgba(0, 0, 0, 0.54)" + "titleColor": "rgba(0, 0, 0, 0.54)", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": "query_builder", + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "titleFont": null }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9", "typeFullFqn": "system.cards.timeseries_table" @@ -4519,7 +4536,7 @@ "name": "{i18n:api-usage.view-statistics}", "buttonType": "icon", "icon": "show_chart", - "buttonColor": "rgba(0, 0, 0, 0.87)", + "buttonColor": "rgba(0, 0, 0, 0.54)", "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", @@ -4955,7 +4972,7 @@ "name": "{i18n:api-usage.view-statistics}", "buttonType": "icon", "icon": "show_chart", - "buttonColor": "rgba(0, 0, 0, 0.87)", + "buttonColor": "rgba(0, 0, 0, 0.54)", "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", From 8bc4d462b8b3f2b8d9c2361f060d23ad83dc315e Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 18 Dec 2025 17:51:51 +0200 Subject: [PATCH 134/147] UI: Add title style --- ui-ngx/src/assets/dashboard/api_usage.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index f4d5892f61..226c570753 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -138,7 +138,14 @@ "color": "rgba(0, 0, 0, 0.38)", "displayTypePrefix": true }, - "titleFont": null + "titleFont": { + "size": 14, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "20px" + } }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9", "typeFullFqn": "system.cards.timeseries_table" From 950656059287aa129309377e6a7b35441a4df489 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 18 Dec 2025 18:52:27 +0200 Subject: [PATCH 135/147] UI: Standardize filter button labels, filter panel and entity naming --- .../alarm-rule-dialog.component.html | 2 +- .../alarm-rule-filter-config.component.html | 4 +- .../alarm-rule-filter-config.component.scss | 1 - .../alarm-rule-filter-config.component.ts | 13 +-- .../alarm-rules/alarm-rules-table-config.ts | 8 +- .../alarm/alarm-filter-config.component.ts | 2 +- .../audit-log/audit-log-filter.component.html | 8 +- .../audit-log/audit-log-filter.component.scss | 9 ++ .../audit-log/audit-log-filter.component.ts | 25 ++++-- .../calculated-fields-table-config.ts | 6 +- .../calculated-field-dialog.component.html | 2 +- ...ulated-fields-filter-config.component.html | 10 +-- ...ulated-fields-filter-config.component.scss | 12 --- ...lculated-fields-filter-config.component.ts | 32 ++++--- .../device/device-info-filter.component.html | 53 +++++++----- .../device/device-info-filter.component.scss | 10 +++ .../device/device-info-filter.component.ts | 85 +++++++++++++------ ...device-profile-autocomplete.component.html | 9 +- .../device-profile-autocomplete.component.ts | 9 +- .../assets/locale/locale.constant-en_US.json | 23 ++--- 20 files changed, 198 insertions(+), 125 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index d9bff11bf0..869b707d98 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -65,7 +65,7 @@ class="flex-1" showLabel required - label="{{ 'alarm-rule.target-entity-type' | translate }}" + label="{{ 'entity.entity-type' | translate }}" [filterAllowedEntityTypes]="false" [allowedEntityTypes]="alarmRuleEntityTypeList" formControlName="entityType"> diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html index 5deddffd87..c9629f00a3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -66,7 +66,7 @@
-
alarm-rule.target-entity-type
+
entity.entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -78,7 +78,7 @@
@if (alarmRuleFilterConfigForm.get('entityType').value) {
-
alarm-rule.target-entities
+
entity.entities
{ this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.target-entity', '30%', + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } - this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '23%' : '67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts index e90071299c..093858a500 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -107,7 +107,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal alarmSeverityTranslationMap = alarmSeverityTranslations; - buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); + buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); alarmFilterConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html index b97285ba07..8d53bdd99b 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html @@ -24,14 +24,14 @@ filter_list{{buttonDisplayValue}} -
-
-
+ +
+
audit-log.filter-types
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss index 77bd5aea60..ca52472aa1 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss @@ -33,3 +33,12 @@ } } } + +::ng-deep { + .tb-audit-log-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts index e78b38965d..f810eb5df7 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -16,16 +16,27 @@ import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; -import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { isDefined, isDefinedAndNotNull } from '@app/core/utils'; import { StringItemsOption } from '@app/shared/components/string-items-list.component'; import { ActionType, actionTypeTranslations, - auditLogFilterEquals, AuditLogFilter, + auditLogFilterEquals, POSITION_MAP, } from '@app/shared/public-api'; import { TranslateService } from '@ngx-translate/core'; @@ -56,9 +67,9 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { actionTypeTranslations = actionTypeTranslations; - buttonDisplayValue = this.translate.instant('audit-log.filter'); + buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title'); - auditLogFilterForm: UntypedFormGroup; + auditLogFilterForm: FormGroup; auditLogOverlayRef: OverlayRef; @@ -68,7 +79,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { private propagateChange = (_: any) => {}; - constructor(private fb: UntypedFormBuilder, + constructor(private fb: FormBuilder, private translate: TranslateService, private overlay: Overlay, private nativeElement: ElementRef, @@ -93,7 +104,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 56a3cc2a45..e5c57aae2e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -136,11 +136,13 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('name', 'common.name', this.pageMode ? '33%' : '60%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityLinkTableColumn('entityName', 'calculated-fields.target-entity', '33%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } - this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '33%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); + this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.cellActionDescriptors.push( { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 44c2e702e5..3d669b02c6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -62,7 +62,7 @@ class="flex-1" showLabel required - label="{{ 'alarm-rule.target-entity-type' | translate }}" + label="{{ 'entity.entity-type' | translate }}" [filterAllowedEntityTypes]="false" [allowedEntityTypes]="calculatedFieldsEntityTypeList" formControlName="entityType"> diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html index b3cc3b387a..5f4efea5b8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html @@ -21,18 +21,18 @@ -
+
-
alarm-rule.target-entity-type
+
entity.entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -80,7 +80,7 @@
@if (cfFilterForm.get('entityType').value) {
-
alarm-rule.target-entities
+
entity.entities
this.translate.instant(CalculatedFieldTypeTranslations.get(type).name)).join(', ')); + } + if (this.cfFilterConfig?.entityType) { + filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.cfFilterConfig.entityType).type)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('calculated-fields.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery { return { types: formValue?.types ?? [], diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html index c7d97aae23..55b99d8a8d 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html @@ -33,6 +33,13 @@
+ + -
- -
+
+
+ audit-log.action-data +
- - -
+
+ audit-log.failure-details +
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss index 57bd26c8ba..5f029ce4f1 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.scss @@ -13,13 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import '../../../../../scss/constants'; + :host { - .tb-audit-log-action-data, - .tb-audit-log-failure-details { - width: 100%; - min-width: 400px; - height: 100%; - min-height: 50px; + display: grid; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + @media #{$mat-gt-xs} { + .mat-mdc-dialog-content { + max-height: 80vh; + } + } + + .tb-audit-log { + min-width: max(400px, 100%); border: 1px solid #c0c0c0; + @media #{$mat-xs} { + min-width: 100%; + } } } diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts index c888fafebc..e3d8bbc582 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts @@ -22,7 +22,11 @@ import { ActionStatus, AuditLog } from '@shared/models/audit-log.models'; import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; +import { Observable, of } from 'rxjs'; +import { isObject } from '@core/utils'; +import { ContentType, contentTypesMap } from '@shared/models/constants'; +import { beautifyJs } from '@shared/models/beautify.models'; export interface AuditLogDetailsDialogData { auditLog: AuditLog; @@ -41,11 +45,10 @@ export class AuditLogDetailsDialogComponent extends DialogComponent, protected router: Router, @@ -58,12 +61,10 @@ export class AuditLogDetailsDialogComponent extends DialogComponent = { - mode: 'ace/mode/java', - theme: 'ace/theme/github', - showGutter: false, - showPrintMargin: false, - readOnly: true - }; - - const advancedOptions = { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }; - - editorOptions = {...editorOptions, ...advancedOptions}; - getAce().subscribe( - (ace) => { - const editor = ace.edit(editorElement, editorOptions); - this.aceEditors.push(editor); - editor.session.setUseWrapMode(false); - editor.setValue(content, -1); - this.updateEditorSize(editorElement, content, editor); - } - ); - } - - updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { - let newHeight = 200; - let newWidth = 600; - if (content && content.length > 0) { - const lines = content.split('\n'); - newHeight = 18 * lines.length + 16; - let maxLineLength = 0; - lines.forEach((row) => { - const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); - const lineLength = line.length; - maxLineLength = Math.max(maxLineLength, lineLength); - }); - newWidth = 9 * maxLineLength + 16; + let mode = 'java'; + let content$: Observable = null; + let contentType = ContentType.TEXT; + if (content && isObject(content)) { + contentType = ContentType.JSON; + mode = contentTypesMap.get(contentType).code; + content$ = beautifyJs(JSON.stringify(content), {indent_size: 2}); + } + if (!content$) { + content$ = of(content as string); } - // newHeight = Math.min(400, newHeight); - this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); - editor.resize(); + content$.subscribe((processedContent) => { + const isJSON = contentType === ContentType.JSON + const editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: isJSON, + showFoldWidgets: true, + foldStyle: 'markbeginend', + showPrintMargin: false, + readOnly: true, + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + }; + getAce().subscribe( + (ace) => { + const editor = ace.edit(editorElement, editorOptions); + this.aceEditors.push(editor); + editor.session.setUseWrapMode(false); + editor.setValue(processedContent, -1); + updateEditorSize(editorElement, processedContent, editor, this.renderer, {showGutter: isJSON}); + } + ) + }); } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html index 27280bfd9e..aa1f8754a5 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html @@ -24,8 +24,8 @@ close -
-
+
+
diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss index 2dd61e2722..ee1ca74151 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss @@ -13,11 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../scss/constants'; + :host { + display: grid; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + @media #{$mat-gt-xs} { + .mat-mdc-dialog-content { + max-height: 80vh; + } + } + .tb-event-content { - width: 100%; min-width: 400px; - height: 100%; - min-height: 50px; + &.border { + border: 1px solid #c0c0c0; + } + @media #{$mat-xs} { + min-width: 100%; + } } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts index 2ae62dd890..9ca6ad8b43 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts @@ -23,7 +23,7 @@ import { Ace } from 'ace-builds'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ContentType, contentTypesMap } from '@shared/models/constants'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; import { Observable } from 'rxjs/internal/Observable'; import { beautifyJs } from '@shared/models/beautify.models'; import { of } from 'rxjs'; @@ -40,40 +40,38 @@ export interface EventContentDialogData { templateUrl: './event-content-dialog.component.html', styleUrls: ['./event-content-dialog.component.scss'] }) -export class EventContentDialogComponent extends DialogComponent implements OnInit, OnDestroy { +export class EventContentDialogComponent extends DialogComponent implements OnInit, OnDestroy { @ViewChild('eventContentEditor', {static: true}) eventContentEditorElmRef: ElementRef; - content: string; title: string; - contentType: ContentType; - aceEditor: Ace.Editor; + showBorder = false; + + private contentType: ContentType; + private aceEditor: Ace.Editor; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: EventContentDialogData, - public dialogRef: MatDialogRef, + protected dialogRef: MatDialogRef, private renderer: Renderer2) { super(store, router, dialogRef); } ngOnInit(): void { - this.content = this.data.content; this.title = this.data.title; this.contentType = this.data.contentType; - this.createEditor(this.eventContentEditorElmRef, this.content); + this.createEditor(this.eventContentEditorElmRef, this.data.content); } ngOnDestroy(): void { - if (this.aceEditor) { - this.aceEditor.destroy(); - } + this.aceEditor?.destroy(); super.ngOnDestroy(); } - isJson(str) { + private isJson(str: string) { try { return isLiteralObject(JSON.parse(str)); } catch (e) { @@ -81,78 +79,53 @@ export class EventContentDialogComponent extends DialogComponent = null; if (this.contentType) { mode = contentTypesMap.get(this.contentType).code; if (this.contentType === ContentType.JSON && content) { - content$ = beautifyJs(content, {indent_size: 4}); + content$ = beautifyJs(content, {indent_size: 2}); } else if (this.contentType === ContentType.BINARY && content) { try { const decodedData = base64toString(content); if (this.isJson(decodedData)) { mode = 'json'; - content$ = beautifyJs(decodedData, {indent_size: 4}); + content$ = beautifyJs(decodedData, {indent_size: 2}); } else { content$ = of(decodedData); } - } catch (e) {} + } catch (e) {/**/} } } if (!content$) { content$ = of(content); } - content$.subscribe( - (processedContent) => { - let editorOptions: Partial = { - mode: `ace/mode/${mode}`, - theme: 'ace/theme/github', - showGutter: false, - showPrintMargin: false, - readOnly: true - }; - - const advancedOptions = { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }; - - editorOptions = {...editorOptions, ...advancedOptions}; - getAce().subscribe( - (ace) => { - this.aceEditor = ace.edit(editorElement, editorOptions); - this.aceEditor.session.setUseWrapMode(false); - this.aceEditor.setValue(processedContent, -1); - this.updateEditorSize(editorElement, processedContent, this.aceEditor); - } - ); - } - ); - } - - updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { - let newHeight = 400; - let newWidth = 600; - if (content && content.length > 0) { - const lines = content.split('\n'); - newHeight = 17 * lines.length + 16; - let maxLineLength = 0; - lines.forEach((row) => { - const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); - const lineLength = line.length; - maxLineLength = Math.max(maxLineLength, lineLength); - }); - newWidth = 9 * maxLineLength + 16; - } - // newHeight = Math.min(400, newHeight); - this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); - editor.resize(); + content$.subscribe((processedContent) => { + const isJSON = mode === 'json' && processedContent !== '{}'; + this.showBorder = isJSON; + const editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: isJSON, + showFoldWidgets: true, + foldStyle: 'markbeginend', + showPrintMargin: false, + readOnly: true, + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + }; + getAce().subscribe( + (ace) => { + this.aceEditor = ace.edit(editorElement, editorOptions); + this.aceEditor.session.setUseWrapMode(false); + this.aceEditor.setValue(processedContent, -1); + updateEditorSize(editorElement, processedContent, this.aceEditor, this.renderer, {showGutter: isJSON}); + } + ) + }); } - } diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts index e549dd03c1..20f8660279 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -15,14 +15,11 @@ /// import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; -import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Ace } from 'ace-builds'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { RafService } from '@core/services/raf.service'; import { isDefinedAndNotNull, isUndefined } from '@core/utils'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-json-object-view', @@ -36,7 +33,7 @@ import { getAce } from '@shared/models/ace/ace.models'; } ] }) -export class JsonObjectViewComponent implements OnInit, OnDestroy { +export class JsonObjectViewComponent implements OnInit, OnDestroy, ControlValueAccessor { @ViewChild('jsonViewer', {static: true}) jsonViewerElmRef: ElementRef; @@ -55,96 +52,54 @@ export class JsonObjectViewComponent implements OnInit, OnDestroy { @Input() sort: (key: string, value: any) => any; - private widthValue: boolean; - - get autoWidth(): boolean { - return this.widthValue; - } - @Input() - set autoWidth(value: boolean) { - this.widthValue = coerceBooleanProperty(value); - } - - private heigthValue: boolean; - - get autoHeight(): boolean { - return this.heigthValue; - } + @coerceBoolean() + autoWidth: boolean @Input() - set autoHeight(value: boolean) { - this.heigthValue = coerceBooleanProperty(value); - } + @coerceBoolean() + autoHeight: boolean - constructor(public elementRef: ElementRef, - protected store: Store, - private raf: RafService, - private renderer: Renderer2) { + constructor(private renderer: Renderer2) { } ngOnInit(): void { this.viewerElement = this.jsonViewerElmRef.nativeElement; - let editorOptions: Partial = { + const editorOptions: Partial = { mode: 'ace/mode/java', theme: 'ace/theme/github', showGutter: false, showPrintMargin: false, - readOnly: true - }; - - const advancedOptions = { + readOnly: true, enableSnippets: false, enableBasicAutocompletion: false, enableLiveAutocompletion: false }; - editorOptions = {...editorOptions, ...advancedOptions}; getAce().subscribe( (ace) => { this.jsonViewer = ace.edit(this.viewerElement, editorOptions); this.jsonViewer.session.setUseWrapMode(false); this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); - if (this.contentValue && (this.widthValue || this.heigthValue)) { - this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + if (this.contentValue && (this.autoWidth || this.autoHeight)) { + updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer, this.renderer, { + ignoreHeight: !this.autoHeight, + ignoreWidth: !this.autoWidth + }); } } ); } ngOnDestroy(): void { - if (this.jsonViewer) { - this.jsonViewer.destroy(); - } + this.jsonViewer?.destroy(); } - updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { - let newHeight = 200; - let newWidth = 600; - if (content && content.length > 0) { - const lines = content.split('\n'); - newHeight = 17 * lines.length + 17; - let maxLineLength = 0; - lines.forEach((row) => { - const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); - const lineLength = line.length; - maxLineLength = Math.max(maxLineLength, lineLength); - }); - newWidth = 8 * maxLineLength + 16; - } - if (this.heigthValue) { - this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); - } - if (this.widthValue) { - this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); - } - editor.resize(); - } registerOnChange(fn: any): void { this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } writeValue(value: any): void { @@ -162,8 +117,11 @@ export class JsonObjectViewComponent implements OnInit, OnDestroy { } if (this.jsonViewer) { this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); - if (this.contentValue && (this.widthValue || this.heigthValue)) { - this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + if (this.contentValue && (this.autoWidth || this.autoHeight)) { + updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer, this.renderer, { + ignoreHeight: !this.autoHeight, + ignoreWidth: !this.autoWidth + }); } } } diff --git a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts index a3d82952cc..90bd9f30ec 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog-csv.component.ts @@ -39,7 +39,7 @@ import { import { ImportExportService } from '@shared/import-export/import-export.service'; import { TableColumnsAssignmentComponent } from '@shared/import-export/table-columns-assignment.component'; import { Ace } from 'ace-builds'; -import { getAce } from '@shared/models/ace/ace.models'; +import { getAce, updateEditorSize } from '@shared/models/ace/ace.models'; export interface ImportDialogCsvData { entityType: EntityType; @@ -283,21 +283,9 @@ export class ImportDialogCsvComponent extends DialogComponent 0) { - const lines = content.split('\n'); - newHeight = 16 * lines.length + 24; - } - const minHeight = Math.min(200, newHeight); - this.renderer.setStyle(editorElement, 'minHeight', minHeight.toString() + 'px'); - this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); - editor.resize(); - } - } diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts index 49cf670989..31b97ff144 100644 --- a/ui-ngx/src/app/shared/models/ace/ace.models.ts +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable'; import { forkJoin, from, of } from 'rxjs'; import { map, mergeMap, tap } from 'rxjs/operators'; import { unwrapModule } from '@core/utils'; +import { Renderer2 } from '@angular/core'; let aceDependenciesLoaded = false; let aceModule: any; @@ -98,6 +99,46 @@ export function getAceDiff(): Observable { } } +export function updateEditorSize(editorElement: any, content: string, editor: Ace.Editor, renderer: Renderer2, options?: { + showGutter?: boolean, + ignoreHeight?: boolean, + ignoreWidth?: boolean, + setMinHeight?: boolean +}): void { + let newHeight = 200; + let newWidth = 600; + if (content && content.length > 0) { + if (editor.renderer.lineHeight <= 0) { + editor.renderer.updateFull(true); + } + const lines = content.split('\n'); + newHeight = editor.renderer.lineHeight * lines.length + 16; + let maxLineLength = 0; + lines.forEach((row) => { + const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); + const lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + }); + if (options?.showGutter) { + maxLineLength += lines.length.toString().length; + } + newWidth = 10 * maxLineLength + 16; + if (options?.showGutter) { + newWidth += 32; + } + } + if (!options.ignoreHeight) { + renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + } + if (options.setMinHeight) { + renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + } + if (!options.ignoreWidth) { + renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + } + editor.resize(); +} + export class Range implements Ace.Range { public start: Ace.Point; From b0c39c4817330bcd8e9871333f69ef26db77dcb6 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 19 Dec 2025 19:34:26 +0100 Subject: [PATCH 143/147] TbMqttSslTransportComponent condition for both tb-transport and monolith mode --- .../server/transport/mqtt/TbMqttSslTransportComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java index 2a0e8ef42f..9f55d40919 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java @@ -27,5 +27,5 @@ import java.lang.annotation.RetentionPolicy; @Inherited @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true' && '${transport.mqtt.ssl.enabled:false}'=='true')") +@ConditionalOnExpression("'${transport.mqtt.ssl.enabled:false}'=='true' && ('${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true'))") public @interface TbMqttSslTransportComponent {} From 706b02295de2c36fcb68648df67d412cfc1a0e75 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Fri, 19 Dec 2025 23:52:57 +0200 Subject: [PATCH 144/147] UI: Fixed edit cf and refactor cf models and service --- .../core/http/calculated-fields.service.ts | 16 +++--- .../alarm-rules/alarm-rules-table-config.ts | 43 +++++++++------- .../calculated-fields-table-config.ts | 50 ++++++++++--------- .../entity/entity-subtype-list.component.ts | 11 ++-- .../shared/models/calculated-field.models.ts | 2 + 5 files changed, 63 insertions(+), 59 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 0fba704277..f3bab62cd2 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -15,17 +15,13 @@ /// import { Injectable } from '@angular/core'; -import { - createDefaultHttpOptions, - defaultHttpOptionsFromConfig, - defaultHttpOptionsFromParams, - RequestConfig -} from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { CalculatedField, + CalculatedFieldInfo, CalculatedFieldsQuery, CalculatedFieldTestScriptInputParams, CalculatedFieldType @@ -56,11 +52,11 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFieldsFilter(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + public getCalculatedFields(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); } - public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { + public getCalculatedFieldsByEntityId({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type} , config)); } @@ -72,7 +68,7 @@ export class CalculatedFieldsService { return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); } - public getAlarmRuleNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable> { + public getCalculatedFieldNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable> { return this.http.get>(`/api/calculatedFields/names${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type}, config)); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index f1bd30d853..46d43f2921 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -43,6 +43,7 @@ import { CalculatedField, CalculatedFieldAlarmRule, CalculatedFieldEventArguments, + CalculatedFieldInfo, CalculatedFieldsQuery, CalculatedFieldType, getCalculatedFieldArgumentsEditorCompleter, @@ -67,12 +68,14 @@ import { CalculatedFieldTestScriptDialogData } from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; -export class AlarmRulesTableConfig extends EntityTableConfig { +type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo; + +export class AlarmRulesTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('action.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + action: (calculatedField: AlarmRuleTableEntity) => this.openDebugEventsDialog.call(this, calculatedField), }; alarmRuleFilterConfig: CalculatedFieldsQuery; @@ -81,11 +84,11 @@ export class AlarmRulesTableConfig extends EntityTableConfig { private translate: TranslateService, private dialog: MatDialog, private datePipe: DatePipe, - public entityId: EntityId = null, + private entityId: EntityId = null, private store: Store, private destroyRef: DestroyRef, private renderer: Renderer2, - public entityName: string, + private entityName: string, private ownerId: EntityId = null, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, @@ -118,7 +121,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedAlarmDialog.bind(this); - this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); + this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); @@ -143,10 +146,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', - entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '23%' : '67%', @@ -193,13 +196,13 @@ export class AlarmRulesTableConfig extends EntityTableConfig { ); } - fetchCalculatedFields(pageLink: PageLink): Observable> { + fetchCalculatedFields(pageLink: PageLink): Observable> { return this.pageMode ? - this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) : - this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); + this.calculatedFieldsService.getCalculatedFields(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) : + this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink, CalculatedFieldType.ALARM); } - onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + onOpenDebugConfig($event: Event, calculatedField: AlarmRuleTableEntity): void { $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { @@ -224,7 +227,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }, $event.target as Element); } - private editCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + private editCalculatedField($event: Event, calculatedField: AlarmRuleTableEntity, isDirty = false): void { $event?.stopPropagation(); this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) .subscribe((res) => { @@ -234,11 +237,12 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }); } - private copyCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + private copyCalculatedField($event: Event, calculatedField: AlarmRuleTableEntity, isDirty = false): void { $event?.stopPropagation(); const copyCalculatedAlarmRule = deepClone(calculatedField); if (this.pageMode) { copyCalculatedAlarmRule.entityId = null; + delete (copyCalculatedAlarmRule as CalculatedFieldInfo).entityName; } delete copyCalculatedAlarmRule.id; this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty) @@ -249,8 +253,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }); } - private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { + private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable { const entityId = this.entityId || value?.entityId; + const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], @@ -258,8 +263,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { value, buttonTitle, entityId, + entityName, tenantId: this.tenantId, - entityName: this.entityName, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, additionalDebugActionConfig: this.additionalDebugActionConfig, isDirty, @@ -271,7 +276,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { .pipe(filter(Boolean)); } - private openDebugEventsDialog($event: Event, calculatedField: CalculatedField): void { + private openDebugEventsDialog($event: Event, calculatedField: AlarmRuleTableEntity): void { $event?.stopPropagation(); this.dialog.open(EventsDialogComponent, { disableClose: true, @@ -290,7 +295,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { .subscribe(); } - private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { + private exportAlarmRule($event: Event, calculatedField: AlarmRuleTableEntity): void { $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); } @@ -343,7 +348,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { ).subscribe(() => this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedField, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { + private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { if (calculatedField.type === CalculatedFieldType.ALARM) { const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const type = calculatedField.configuration.arguments[key].refEntityKey.type; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index e5c57aae2e..8385b5cb45 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -40,8 +40,8 @@ import { ArgumentEntityType, ArgumentType, CalculatedField, - CalculatedFieldAlarmRule, CalculatedFieldEventArguments, + CalculatedFieldInfo, CalculatedFieldScriptConfiguration, CalculatedFieldsQuery, CalculatedFieldType, @@ -68,12 +68,14 @@ import { CalculatedFieldsHeaderComponent } from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; + +export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('action.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, null, calculatedField), + action: (calculatedField: CalculatedFieldsTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), }; calculatedFieldFilterConfig: CalculatedFieldsQuery; @@ -82,11 +84,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig, private destroyRef: DestroyRef, private renderer: Renderer2, - public entityName: string, + private entityName: string, private ownerId: EntityId = null, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, @@ -110,7 +112,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedFieldDialog.bind(this); - this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); + this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); @@ -136,10 +138,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('name', 'common.name', this.pageMode ? '33%' : '60%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', - entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); @@ -180,13 +182,13 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { + fetchCalculatedFields(pageLink: PageLink): Observable> { return this.pageMode ? - this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, this.calculatedFieldFilterConfig): - this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); + this.calculatedFieldsService.getCalculatedFields(pageLink, this.calculatedFieldFilterConfig): + this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink); } - onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void { $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { @@ -210,7 +212,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -220,8 +222,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false): Observable { const entityId = this.entityId || value?.entityId; + const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], @@ -229,8 +232,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { return (calculatedField.type === CalculatedFieldType.SCRIPT || @@ -274,19 +277,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (res) { this.updateData(); @@ -350,7 +354,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + private getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { if ( calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index dc89f05fde..05511095cf 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs'; import { debounceTime, map, mergeMap, share } from 'rxjs/operators'; @@ -46,7 +46,7 @@ import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; } ] }) -export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { +export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, OnDestroy { entitySubtypeListFormGroup: FormGroup; @@ -119,7 +119,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, private dirty = false; - private propagateChange = (v: any) => { }; + private propagateChange = (_v: any) => { }; private hasPageDataEntitySubTypes = new Set([ EntityType.ALARM, @@ -225,9 +225,6 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, ); } - ngAfterViewInit(): void { - } - ngOnDestroy(): void { if (this.broadcastSubscription) { this.broadcastSubscription.unsubscribe(); @@ -335,7 +332,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, subTypesPagesObservable = this.alarmService.getAlarmTypes(pageLink, {ignoreLoading: true}); break; case EntityType.CALCULATED_FIELD: - subTypesCfPagesObservable = this.calculatedFieldsService.getAlarmRuleNames(pageLink, CalculatedFieldType.ALARM, {ignoreLoading: true}); + subTypesCfPagesObservable = this.calculatedFieldsService.getCalculatedFieldNames(pageLink, CalculatedFieldType.ALARM, {ignoreLoading: true}); } if (subTypesPagesObservable) { this.entitySubtypes = subTypesPagesObservable.pipe( diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 5f07374cf8..825c08a69f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -79,6 +79,8 @@ export type CalculatedField = | CalculatedFieldRelatedEntityAggregation | CalculatedFieldAlarmRule; +export type CalculatedFieldInfo = CalculatedField & {entityName: string}; + export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', From 6827949e7fee59e2c335f750b98e09148876b000 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 22 Dec 2025 11:25:26 +0200 Subject: [PATCH 145/147] UI: Fixed color settings panel update --- .../lib/settings/common/color-range-list.component.ts | 7 ------- .../settings/common/color-settings-panel.component.html | 2 +- .../lib/settings/common/color-settings-panel.component.ts | 7 ------- .../widget/lib/settings/common/gradient.component.ts | 5 ----- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts index f7c68fec3d..84d3719ef5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts @@ -113,9 +113,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On this.colorRangeListFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => this.updateModel()); - this.colorRangeListFormGroup.get('advancedMode').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0)); } ngOnDestroy() { @@ -179,7 +176,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On public removeAdvancedRange(index: number) { (this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index); - setTimeout(() => {this.popover?.updatePosition();}, 0); } get advancedRangeFormArray(): UntypedFormArray { @@ -193,7 +189,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On removeRange(index: number) { this.rangeListFormArray.removeAt(index); this.colorRangeListFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } rangeDrop(event: CdkDragDrop, range: string) { @@ -216,7 +211,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray; const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]); advancedRangeColorsArray.push(advancedRangeColorControl); - setTimeout(() => {this.popover?.updatePosition();}, 0); } addRange() { @@ -228,7 +222,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On }; this.rangeListFormArray.push(this.colorRangeControl(newRange)); this.colorRangeListFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html index d4e71096b4..bfd6a674b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html @@ -94,7 +94,7 @@
-
+
{ - this.updateValidators(); - setTimeout(() => {this.popover?.updatePosition();}, 0); - }); this.updateValidators(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts index 9078c8db2b..6eca820e27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts @@ -117,9 +117,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => this.updateModel()); - this.gradientFormGroup.get('advancedMode').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0)); } ngOnDestroy() { @@ -236,7 +233,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientListFormArray.removeAt(index); } this.gradientFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } gradientDrop(event: CdkDragDrop, advanced = false) { @@ -255,7 +251,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)')); } this.gradientFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); } updateModel() { From cb7b2e2705d762149d61f43e0388e428e84abba6 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko <83352633+ArtemDzhereleiko@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:07:32 +0200 Subject: [PATCH 146/147] Fixed see debug event and change color alarm severity for alarm table (#14666) * UI: Fixed see debug event * UI: Fixed alarm colors --------- Co-authored-by: ArtemDzhereleiko --- .../components/alarm-rules/alarm-rules-table-config.ts | 2 +- .../alarm-rules/create-cf-alarm-rules.component.ts | 5 ++--- .../components/notification/notification.component.ts | 9 ++++----- ui-ngx/src/app/shared/models/alarm.models.ts | 10 +++++----- ui-ngx/src/app/shared/models/notification.models.ts | 10 ---------- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 46d43f2921..3d1c066839 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -75,7 +75,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.openDebugEventsDialog.call(this, calculatedField), + action: (calculatedField: AlarmRuleTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), }; alarmRuleFilterConfig: CalculatedFieldsQuery; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 8959d709fa..09a2d52756 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -27,10 +27,9 @@ import { Validator, Validators } from '@angular/forms'; -import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { AlarmSeverity, alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmRule } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; -import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @@ -68,7 +67,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida alarmSeverityEnum = AlarmSeverity; alarmSeverityTranslationMap = alarmSeverityTranslations; - AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; + AlarmSeverityNotificationColors = alarmSeverityColors; createAlarmRulesFormGroup = this.fb.group({ createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts index 77be7ff7cd..204c9e3230 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.ts +++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts @@ -17,7 +17,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ActionButtonLinkType, - AlarmSeverityNotificationColors, Notification, NotificationStatus, NotificationType, @@ -25,7 +24,7 @@ import { } from '@shared/models/notification.models'; import { UtilsService } from '@core/services/utils.service'; import { Router } from '@angular/router'; -import { alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models'; import tinycolor from 'tinycolor2'; import { StateObject } from '@core/api/widget-api.models'; import { objToBase64URI } from '@core/utils'; @@ -135,12 +134,12 @@ export class NotificationComponent implements OnInit { } alarmColorSeverity(alpha: number) { - return tinycolor(AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)).setAlpha(alpha).toRgbString(); + return tinycolor(alarmSeverityColors.get(this.notification.info.alarmSeverity)).setAlpha(alpha).toRgbString(); } notificationColor(): string { if (this.notification.type === NotificationType.ALARM && !this.notification.info.cleared) { - return AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity); + return alarmSeverityColors.get(this.notification.info.alarmSeverity); } return 'transparent'; } @@ -154,7 +153,7 @@ export class NotificationComponent implements OnInit { notificationIconColor(): object { if (this.notification.type === NotificationType.ALARM) { - return {color: AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)}; + return {color: alarmSeverityColors.get(this.notification.info.alarmSeverity)}; } else if (this.notification.type === NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) { return {color: '#D12730'}; } else if (this.notification.type === NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) { diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 5e1e5def25..fda79a0154 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -88,11 +88,11 @@ export const alarmSearchStatusTranslations = new Map( export const alarmSeverityColors = new Map( [ - [AlarmSeverity.CRITICAL, 'red'], - [AlarmSeverity.MAJOR, 'orange'], - [AlarmSeverity.MINOR, '#ffca3d'], - [AlarmSeverity.WARNING, '#abab00'], - [AlarmSeverity.INDETERMINATE, 'green'] + [AlarmSeverity.CRITICAL, '#D12730'], + [AlarmSeverity.MAJOR, '#FEAC0C'], + [AlarmSeverity.MINOR, '#F2DA05'], + [AlarmSeverity.WARNING, '#F66716'], + [AlarmSeverity.INDETERMINATE, '#00000061'] ] ); diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index f9abf6462b..352e49e01e 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -551,16 +551,6 @@ export const NotificationTypeIcons = new Map([ [NotificationType.RESOURCES_SHORTAGE, 'warning'] ]); -export const AlarmSeverityNotificationColors = new Map( - [ - [AlarmSeverity.CRITICAL, '#D12730'], - [AlarmSeverity.MAJOR, '#FEAC0C'], - [AlarmSeverity.MINOR, '#F2DA05'], - [AlarmSeverity.WARNING, '#F66716'], - [AlarmSeverity.INDETERMINATE, '#00000061'] - ] -); - export enum ActionButtonLinkType { LINK = 'LINK', DASHBOARD = 'DASHBOARD' From 934b54180be8e60a8f4af7d13bf2b3cee97ad5d5 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 23 Dec 2025 12:20:23 +0200 Subject: [PATCH 147/147] Fixed disabling update button based on filters change (#14669) --- .../alarm-rules/alarm-rule-filter-config.component.ts | 1 + .../home/components/alarm/alarm-filter-config.component.ts | 1 + .../home/components/audit-log/audit-log-filter.component.ts | 2 ++ .../table-header/calculated-fields-filter-config.component.ts | 1 + .../home/components/device/device-info-filter.component.ts | 1 + 5 files changed, 6 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts index 03c6932067..a624a5ae12 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts @@ -196,6 +196,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces cancel() { this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); + this.alarmRuleFilterConfigForm.markAsPristine(); if (this.overlayRef) { this.overlayRef.dispose(); } else { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts index 093858a500..e938b6fa19 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -224,6 +224,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal cancel() { this.updateAlarmConfigForm(this.alarmFilterConfig); + this.alarmFilterConfigForm.markAsPristine(); if (this.overlayRef) { this.overlayRef.dispose(); } else { diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts index f810eb5df7..18e5f1a1db 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -163,11 +163,13 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { cancel() { this.updateAuditLogFilterForm(this.auditLogFilter); + this.auditLogFilterForm.markAsPristine(); this.auditLogOverlayRef.dispose(); } update() { this.auditLogFilterUpdated(this.auditLogFilterForm.value); + this.auditLogFilterForm.markAsPristine(); this.auditLogOverlayRef.dispose(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts index 83ea05af4c..ffe986d434 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts @@ -204,6 +204,7 @@ export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlVal cancel() { this.updateCfConfigForm(this.cfFilterConfig); + this.cfFilterForm.markAsPristine(); if (this.overlayRef) { this.overlayRef.dispose(); } else { diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts index d6a81883d2..531323bf5c 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts @@ -196,6 +196,7 @@ export class DeviceInfoFilterComponent implements OnInit, ControlValueAccessor { cancel() { this.updateDeviceInfoFilterForm(this.deviceInfoFilter); + this.deviceInfoFilterForm.markAsPristine(); if (this.overlayRef) { this.overlayRef.dispose(); } else {